packages feed

mariadb-haskell (empty) → 0.1.0.0

raw patch · 169 files changed

+37767/−0 lines, 169 filesdep +QuickCheckdep +asn1-encodingdep +asn1-types

Dependencies added: QuickCheck, asn1-encoding, asn1-types, attoparsec, base, base16-bytestring, binary, blaze-textual, bytestring, bytestring-lexing, case-insensitive, containers, criterion, crypton, crypton-pem, crypton-x509, crypton-x509-store, crypton-x509-system, data-default-class, deepseq, directory, filepath, http-types, io-streams, mariadb-haskell, monad-loops, network, process, quickcheck-instances, ram, scanner, scientific, tasty, tasty-expected-failure, tasty-hunit, tasty-quickcheck, text, time, tls, unordered-containers, vector, word-compat, zlib

Files

+ ChangeLog.md view
@@ -0,0 +1,309 @@+# Revision history++## mariadb-haskell 0.1.0.0 — alpha candidate preparation++Alpha: extensive automated validation, without real-world application validation.+Public Hackage publication awaits Anton's review of the candidate documentation+and explicit approval.++MariaDB-only fork of [mysql-haskell](https://github.com/winterland1989/mysql-haskell),+maintained by Anton Antich. This starts an independent release series; the+upstream changelog below is retained as history. Public `Database.MySQL.*`+module names remain, with no MySQL server support target.++* Bounded framing/metadata/cleanup and explicit command/stream ownership,+  cancellation retirement, generation-safe handles and uncertain-COMMIT diagnostics.+* MariaDB extended capability negotiation, utf8mb4 defaults, strict session-aware+  bindings, raw dates, unsigned 64-bit IDs and exact fixed-width Field conversions.+* Verified TLS/SNI, client certificates, native/ed25519/PARSEC/SHA-2 authentication,+  explicit PAM dialogs and principal-pinned Kerberos GSSAPI.+* Managed prepared statements, complete multiple/OUT results, native binary bulk+  with indicators/per-unit results and cached/extended metadata.+* Session/change observations and reset/restoration with exclusive initialization.+* One-shot long-data sources, server cursor batches, progress callbacks and opt-in+  zlib transport compression.+* Stateful bounded CDC decoding, checksums/compressed events, MariaDB GTID resume,+  complete-group checkpoints and consumer-controlled semisync ACKs.+* Full correctness/benchmark history with frozen original/feature references;+  retained Mac/Linux validation: 844 passing records and 63 Mac profiles.+* Independent package identity, GHC 9.4+ bounds, opt-in developer executable,+  native-dependency/source-archive CI, a verified November 2026 Mozilla CA snapshot+  with MPL-2.0 notices, and password-redacted connection diagnostics.+* Packaged user, migration and benchmark guides with compilable examples and+  expanded Haddock ownership/lifetime/error contracts.++See the [migration guide](https://github.com/aantich/mariadb-haskell/blob/main/docs/MIGRATION.md)+for behavior changes, and [release readiness](https://github.com/aantich/mariadb-haskell/blob/main/docs/RELEASING.md)+for final validation evidence and Hackage candidate/publication status.++## Upstream mysql-haskell history++## 1.3.3 -- 2026.10.07++New functions, and fixes to `executeMany` and `executeMany_` for inputs that+left 1.3.2's connection out of step. `queryMulti_` and `queryMulti` return every+result of a CALL or a multi-statement query in order: each result set with its+columns and rows, and each statement's `OK`.++`executeMany` and `executeMany_` now read the whole reply. A statement that+returns rows raises `ExtraResultSets`, where 1.3.2 threw `UnexpectedPacket` or+left the rows for the next command. A query with several statements per+parameter set gives one `OK` per statement, where 1.3.2 returned one per+parameter set and left the rest. `executeMany` with no parameter sets returns+`[]` without contacting the server, where 1.3.2 sent an empty query whose error+reached the next command.+++ Every result set of a CALL or multi-statement query through `queryMulti_`+  [#94](https://github.com/winterland1989/mysql-haskell/pull/94)++## 1.3.2 -- 2026.10.07++Requires crypton 2.0 or newer, the first crypton with the timing side-channel+fixes that TLS connections reach; a project held to crypton 1.x keeps+resolving to 1.3.1. The only API addition is the `ExtraResultSets` exception.+The query functions now return an empty result for an INSERT, UPDATE or DELETE+instead of hanging. `execute_`, `executeStmt` and the query functions read the+whole reply of a CALL or a multi-statement query, so they no longer leave+results behind for the next command; `executeMany` and `executeMany_` are+unchanged (#93).++Errors change only for statements that produce several results, or rows where+`execute_` and `executeStmt` expect none. Those two raise `ExtraResultSets` for+a result set (1.3.1 threw `UnexpectedPacket` when it came first and returned the+`OK` when it came later); the query functions raise it after the first result+set's rows when a second one follows, which 1.3.1 left on the connection. A+later statement that fails raises its `ERRException` from that call or its row+stream, also ahead of `ExtraResultSets`. Stack users who set the removed+`crypton-1-1` flag must delete it from `stack.yaml`, since Stack rejects flags a+package does not define.+++ Query functions no longer hang on statements without a result set,+  multi-result replies are read to the end, and crypton 2 is required+  [#91](https://github.com/winterland1989/mysql-haskell/pull/91)++ CI: the cabal job builds and runs the test suite+  [#90](https://github.com/winterland1989/mysql-haskell/pull/90)++## 1.3.1 -- 2026.10.06++ Allow crypton 2.0 and 2.1 (commercialhaskell/stackage#8128). The library+  code needed no change; CI now builds and tests against every crypton+  series the bounds admit (0.34, 1.0, 1.1, 2.0 and 2.1).++## 1.3.0 -- 2026.07.22++ Add `Database.MySQL.Field` with a `Field` typeclass for converting+  individual Haskell values to and from `MySQLValue` (#86). One combined+  class in the style of persistent's `PersistField`, with a `DecodeError`+  sum type for decode failures and named top-level encode/decode functions+  backing every instance.++## 1.2.5 -- 2026.05.04++ Replace abandoned `pem` dependency with `crypton-pem` (#84)++## 1.2.4 -- 2026.04.20++ Fix benchmark compilation failures (#82):+  + Replace deleted `Data.Binary.Parser.Char8` import in benchmark with+    `Word8`-based combinators from `Data.Binary.Parser`+  + Replace `GHC.Prim`/`GHC.Types` imports with `GHC.Exts` from `base`+  + Fix `Word8#` vs `Word#` type mismatch in `mhDigitToInt`+  + Remove stale haddock reference to deleted `Char8` module++## 1.2.3 -- 2026.04.12++ Support caching_sha2_password full auth via RSA on non-TLS connections#81 thanks @ikaro1192++## 1.2.2 -- 2026.03.25++ Widen tls upper bound to allow tls 2.4.x++ Fix unsafe ByteString operations (`unsafeDrop`, `unsafeTail`, `unsafeIndex`)+  on untrusted wire protocol data that caused undefined behavior (segfaults,+  garbage reads) on malformed input. The safe alternatives are also O(1).+  Affected parsers: text protocol timestamp/datetime/time/date fields,+  NEWDECIMAL in binlog, and `eventHeaderLen` for unknown event types.++## 1.2.1 -- 2026.03.23++ Export `connectUnixSocket` function.++ Tweak cabal file to better handle crypton vs cryptonite.++ Fix version cut off for tls compatibility.++ Fix missing `liftA2` import when running tests on GHC 9.4.++ Add integration test for Unix socket.++## 1.2.0 -- 2026.03.07++ Add support for  caching_sha2_password authentiation.++## 1.1.9 -- 2026.03.07++ Fix binary protocol error 1210 on modern MySQL/MariaDB:+  `MYSQL_TYPE_BIT` and `MYSQL_TYPE_YEAR` are not valid parameter types+  for `COM_STMT_EXECUTE`. Send `MySQLBit` as `MYSQL_TYPE_LONGLONG`+  (unsigned, little-endian) and `MySQLYear` as `MYSQL_TYPE_SHORT`+  (unsigned, 2-byte integer) instead.++ Fix `MySQLYear` binary encoding: was sending a length-encoded string+  which caused data misalignment for all subsequent parameters.++ Fix version detection for MariaDB 10+/11+ (fractional seconds support).++ Fix CI: enable binary logging and grant binlog privileges for+  integration tests on MariaDB.++ Add roundtrip integration tests for `MySQLBit` and `MySQLYear`.++## 1.1.8++ bump constraints++## 1.1.7 -- 2025.08.23 ++ bump constraints++ upgrade cabal file++ add upper bounds to all the warnings+  + it was only the test suite so I made them broad.++ Disable inline-rule-shadowing for Int24 and Word24.+  those rules fire fine. you can test it our yourself with+  the Main.hs in the main repository.+  There is a stanza to do so in cabal.++ clean up cabal file a bit, use GHC2021++## 1.1.6 -- 2024.10.23 ++ bump constraints++## 1.1.5 -- 2024.06.25++ bump constraints++## 1.1.4 -- 2024.02.17 ++ bump constraints++## 1.1.3 -- 2023.08.30++ bump constraints++## 1.1.2 -- 2023.08.14+++ Fix package name of changelog++ Drop support for RC4 chipher which is depracated++ drop dependency on binary-ieee754, which was unused.++ Fix text 2 support, thanks @RikvanToor++## 1.1.1 -- 2023.08.14+++ cleaned up some warnings++ Merge back into mysql-haskell after gaining hackage access.++ Deprecate mysql-pure in favor of old hackage+  since it's only been out for a day this sort off+  stream lines upgrading for most applications.+  Cabal will just figure it out, rather then+  users having to "find" mysql-pure.+  I'll just make a bonus announcement to+  let people not depend on mysql-pure.++## 1.1.0 -- 2023.08.12 +There was a bunch of stuff unrelated to mysql+which I purged.+If you need any on these go depend on the +respective unmaintained package.+++ Delete module System.IO.Streams.UnixSocket++ Dleete module Data.Binary.Parser.Char8++ Delete module System.IO.Streams.Binary++## 1.0.2 -- 2023.08.12 ++ Bump dependencies, go all into crypton++ merge tcp-streams into the package++## 1.0.1 -- 2023.08.12 ++ add json testfiles as extra source files to make tests pass in nix builds++## 1.0.0 -- 2023.08.12 +++ Fork from mysql-haskell into mysql-pure++ add flake++ merge packages:+  + word24+  + binary-parsers+  + wirestreams+  +  This involved copying over all source files,+  furthermore I copied in all tests and benchmarks.+  The tests are now one giant test suite.+  I temporarly disabled the mysql tests as they need a mysql+  database to run which won't work nicely with CI right now.+  However you can run these locally by uncommenting that line.++ Add CI which relies on native cabal instead of stack++ Add an action to automatically bump version.++ Add nightly build cron job.++## 0.8.4.3 -- 2020-11-04++* Fix build with GHC 8.8.++## 0.8.4.2 -- 2019-01-22++* Fix [stackage#4312](https://github.com/commercialhaskell/stackage/issues/4312): Relax `network` bounds.++## 0.8.4.1 -- 2018-10-23++* Relax `tasty` version bound to build with latest stackage. [#26](https://github.com/winterland1989/mysql-haskell/pull/26)++## 0.8.4.0  -- 2018-10-23++* Add `executeMany_` to execute batch SQLs, [#26](https://github.com/winterland1989/mysql-haskell/issues/26).+* Optimize connection closing sequence, [#20](https://github.com/winterland1989/mysql-haskell/pull/20), [#25](https://github.com/winterland1989/mysql-haskell/pull/25).++## 0.8.3.0  -- 2017-10-09++* Remove unnecessary exports from `Database.MySQL.Base`.+* Reuse TCP connection when using TLS.+* Clean up some compiler warnings.++## 0.8.2.0  -- 2017-10-09++Courtesy of naushadh, `mysql-haskell` will be on stackage again.++* Update to use `tcp-streams-1.x`.+* Fix compatibility with new  `tls/memory` version.++## 0.8.1.0  -- 2016-11-09++* Add `Show` instance to `ConnectInfo`.+* Add proper version bound for `binary`.++## 0.8.0.0  -- 2016-11-09++* Add `ciCharset` field to support `utf8mb4` charset.+* Add `BitMap` field to `COM_STMT_EXECUTE`, and [#8](https://github.com/winterland1989/mysql-haskell/pull/8) by [alexbiehl](https://github.com/alexbiehl).++## 0.7.1.0 -- 2016-11-21++* Add `QueryParam` class and `Param` datatype for multi-valued parameter(s) by [naushadh](https://github.com/naushadh).++## 0.7.0.0 -- 2016-11-09++* Split openssl support to [mysql-haskell-openssl](http://hackage.haskell.org/package/mysql-haskell-openssl).+* Expose `Database.MySQL.Connection` module due to this split, it shouldn't be used by user directly.++## 0.6.0.0 -- 2016-10-25++* Use binary-ieee754 for older binary compatibility.+* Clean up `Database.MySQL.Protocol.MySQLValue` 's export.++## 0.5.1.0 -- 2016-10-20++* Add `queryVector`, `queryVector_` and `queryStmtVector`.+* Use binary-parsers to speed up binary parsers.++## 0.5.0.0 -- 2016-8-22++* Export exception types.+* Fix a regression cause password authentication failed, add tests.+* Fix a reading order bug cause 'prepareStmt/prepareStmtDetail' failed.++## 0.4.0.0 -- 2016-8-22++* Enable TLS support via `tls` package, add benchmarks.++## 0.3.0.0  -- 2016-8-22++* Fix tls connection, change TLS implementation to HsOpenSSL, add benchmarks.+* Fix a bug in 'putLenEncInt' which cause sending large field fail.+* Various optimizations.++## 0.2.0.0  -- 2016-8-19++* Fix OK packet decoder.+* Fix sending large packet(>16M).+* Add `executeMany`, `withTransaction` to Base module.+* Add timestamp field to `RowBinLogEvent`.+* Add test, add insert benchmark.++## 0.1.0.0  -- 2016-8-16++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2016, Winterland++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * 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.++    * Neither the name of winterland1989 nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS 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.
+ LICENSE-MPL-2.0 view
@@ -0,0 +1,373 @@+Mozilla Public License Version 2.0+==================================++1. Definitions+--------------++1.1. "Contributor"+    means each individual or legal entity that creates, contributes to+    the creation of, or owns Covered Software.++1.2. "Contributor Version"+    means the combination of the Contributions of others (if any) used+    by a Contributor and that particular Contributor's Contribution.++1.3. "Contribution"+    means Covered Software of a particular Contributor.++1.4. "Covered Software"+    means Source Code Form to which the initial Contributor has attached+    the notice in Exhibit A, the Executable Form of such Source Code+    Form, and Modifications of such Source Code Form, in each case+    including portions thereof.++1.5. "Incompatible With Secondary Licenses"+    means++    (a) that the initial Contributor has attached the notice described+        in Exhibit B to the Covered Software; or++    (b) that the Covered Software was made available under the terms of+        version 1.1 or earlier of the License, but not also under the+        terms of a Secondary License.++1.6. "Executable Form"+    means any form of the work other than Source Code Form.++1.7. "Larger Work"+    means a work that combines Covered Software with other material, in+    a separate file or files, that is not Covered Software.++1.8. "License"+    means this document.++1.9. "Licensable"+    means having the right to grant, to the maximum extent possible,+    whether at the time of the initial grant or subsequently, any and+    all of the rights conveyed by this License.++1.10. "Modifications"+    means any of the following:++    (a) any file in Source Code Form that results from an addition to,+        deletion from, or modification of the contents of Covered+        Software; or++    (b) any new file in Source Code Form that contains any Covered+        Software.++1.11. "Patent Claims" of a Contributor+    means any patent claim(s), including without limitation, method,+    process, and apparatus claims, in any patent Licensable by such+    Contributor that would be infringed, but for the grant of the+    License, by the making, using, selling, offering for sale, having+    made, import, or transfer of either its Contributions or its+    Contributor Version.++1.12. "Secondary License"+    means either the GNU General Public License, Version 2.0, the GNU+    Lesser General Public License, Version 2.1, the GNU Affero General+    Public License, Version 3.0, or any later versions of those+    licenses.++1.13. "Source Code Form"+    means the form of the work preferred for making modifications.++1.14. "You" (or "Your")+    means an individual or a legal entity exercising rights under this+    License. For legal entities, "You" includes any entity that+    controls, is controlled by, or is under common control with You. For+    purposes of this definition, "control" means (a) the power, direct+    or indirect, to cause the direction or management of such entity,+    whether by contract or otherwise, or (b) ownership of more than+    fifty percent (50%) of the outstanding shares or beneficial+    ownership of such entity.++2. License Grants and Conditions+--------------------------------++2.1. Grants++Each Contributor hereby grants You a world-wide, royalty-free,+non-exclusive license:++(a) under intellectual property rights (other than patent or trademark)+    Licensable by such Contributor to use, reproduce, make available,+    modify, display, perform, distribute, and otherwise exploit its+    Contributions, either on an unmodified basis, with Modifications, or+    as part of a Larger Work; and++(b) under Patent Claims of such Contributor to make, use, sell, offer+    for sale, have made, import, and otherwise transfer either its+    Contributions or its Contributor Version.++2.2. Effective Date++The licenses granted in Section 2.1 with respect to any Contribution+become effective for each Contribution on the date the Contributor first+distributes such Contribution.++2.3. Limitations on Grant Scope++The licenses granted in this Section 2 are the only rights granted under+this License. No additional rights or licenses will be implied from the+distribution or licensing of Covered Software under this License.+Notwithstanding Section 2.1(b) above, no patent license is granted by a+Contributor:++(a) for any code that a Contributor has removed from Covered Software;+    or++(b) for infringements caused by: (i) Your and any other third party's+    modifications of Covered Software, or (ii) the combination of its+    Contributions with other software (except as part of its Contributor+    Version); or++(c) under Patent Claims infringed by Covered Software in the absence of+    its Contributions.++This License does not grant any rights in the trademarks, service marks,+or logos of any Contributor (except as may be necessary to comply with+the notice requirements in Section 3.4).++2.4. Subsequent Licenses++No Contributor makes additional grants as a result of Your choice to+distribute the Covered Software under a subsequent version of this+License (see Section 10.2) or under the terms of a Secondary License (if+permitted under the terms of Section 3.3).++2.5. Representation++Each Contributor represents that the Contributor believes its+Contributions are its original creation(s) or it has sufficient rights+to grant the rights to its Contributions conveyed by this License.++2.6. Fair Use++This License is not intended to limit any rights You have under+applicable copyright doctrines of fair use, fair dealing, or other+equivalents.++2.7. Conditions++Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted+in Section 2.1.++3. Responsibilities+-------------------++3.1. Distribution of Source Form++All distribution of Covered Software in Source Code Form, including any+Modifications that You create or to which You contribute, must be under+the terms of this License. You must inform recipients that the Source+Code Form of the Covered Software is governed by the terms of this+License, and how they can obtain a copy of this License. You may not+attempt to alter or restrict the recipients' rights in the Source Code+Form.++3.2. Distribution of Executable Form++If You distribute Covered Software in Executable Form then:++(a) such Covered Software must also be made available in Source Code+    Form, as described in Section 3.1, and You must inform recipients of+    the Executable Form how they can obtain a copy of such Source Code+    Form by reasonable means in a timely manner, at a charge no more+    than the cost of distribution to the recipient; and++(b) You may distribute such Executable Form under the terms of this+    License, or sublicense it under different terms, provided that the+    license for the Executable Form does not attempt to limit or alter+    the recipients' rights in the Source Code Form under this License.++3.3. Distribution of a Larger Work++You may create and distribute a Larger Work under terms of Your choice,+provided that You also comply with the requirements of this License for+the Covered Software. If the Larger Work is a combination of Covered+Software with a work governed by one or more Secondary Licenses, and the+Covered Software is not Incompatible With Secondary Licenses, this+License permits You to additionally distribute such Covered Software+under the terms of such Secondary License(s), so that the recipient of+the Larger Work may, at their option, further distribute the Covered+Software under the terms of either this License or such Secondary+License(s).++3.4. Notices++You may not remove or alter the substance of any license notices+(including copyright notices, patent notices, disclaimers of warranty,+or limitations of liability) contained within the Source Code Form of+the Covered Software, except that You may alter any license notices to+the extent required to remedy known factual inaccuracies.++3.5. Application of Additional Terms++You may choose to offer, and to charge a fee for, warranty, support,+indemnity or liability obligations to one or more recipients of Covered+Software. However, You may do so only on Your own behalf, and not on+behalf of any Contributor. You must make it absolutely clear that any+such warranty, support, indemnity, or liability obligation is offered by+You alone, and You hereby agree to indemnify every Contributor for any+liability incurred by such Contributor as a result of warranty, support,+indemnity or liability terms You offer. You may include additional+disclaimers of warranty and limitations of liability specific to any+jurisdiction.++4. Inability to Comply Due to Statute or Regulation+---------------------------------------------------++If it is impossible for You to comply with any of the terms of this+License with respect to some or all of the Covered Software due to+statute, judicial order, or regulation then You must: (a) comply with+the terms of this License to the maximum extent possible; and (b)+describe the limitations and the code they affect. Such description must+be placed in a text file included with all distributions of the Covered+Software under this License. Except to the extent prohibited by statute+or regulation, such description must be sufficiently detailed for a+recipient of ordinary skill to be able to understand it.++5. Termination+--------------++5.1. The rights granted under this License will terminate automatically+if You fail to comply with any of its terms. However, if You become+compliant, then the rights granted under this License from a particular+Contributor are reinstated (a) provisionally, unless and until such+Contributor explicitly and finally terminates Your grants, and (b) on an+ongoing basis, if such Contributor fails to notify You of the+non-compliance by some reasonable means prior to 60 days after You have+come back into compliance. Moreover, Your grants from a particular+Contributor are reinstated on an ongoing basis if such Contributor+notifies You of the non-compliance by some reasonable means, this is the+first time You have received notice of non-compliance with this License+from such Contributor, and You become compliant prior to 30 days after+Your receipt of the notice.++5.2. If You initiate litigation against any entity by asserting a patent+infringement claim (excluding declaratory judgment actions,+counter-claims, and cross-claims) alleging that a Contributor Version+directly or indirectly infringes any patent, then the rights granted to+You by any and all Contributors for the Covered Software under Section+2.1 of this License shall terminate.++5.3. In the event of termination under Sections 5.1 or 5.2 above, all+end user license agreements (excluding distributors and resellers) which+have been validly granted by You or Your distributors under this License+prior to termination shall survive termination.++************************************************************************+*                                                                      *+*  6. Disclaimer of Warranty                                           *+*  -------------------------                                           *+*                                                                      *+*  Covered Software is provided under this License on an "as is"       *+*  basis, without warranty of any kind, either expressed, implied, or  *+*  statutory, including, without limitation, warranties that the       *+*  Covered Software is free of defects, merchantable, fit for a        *+*  particular purpose or non-infringing. The entire risk as to the     *+*  quality and performance of the Covered Software is with You.        *+*  Should any Covered Software prove defective in any respect, You     *+*  (not any Contributor) assume the cost of any necessary servicing,   *+*  repair, or correction. This disclaimer of warranty constitutes an   *+*  essential part of this License. No use of any Covered Software is   *+*  authorized under this License except under this disclaimer.         *+*                                                                      *+************************************************************************++************************************************************************+*                                                                      *+*  7. Limitation of Liability                                          *+*  --------------------------                                          *+*                                                                      *+*  Under no circumstances and under no legal theory, whether tort      *+*  (including negligence), contract, or otherwise, shall any           *+*  Contributor, or anyone who distributes Covered Software as          *+*  permitted above, be liable to You for any direct, indirect,         *+*  special, incidental, or consequential damages of any character      *+*  including, without limitation, damages for lost profits, loss of    *+*  goodwill, work stoppage, computer failure or malfunction, or any    *+*  and all other commercial damages or losses, even if such party      *+*  shall have been informed of the possibility of such damages. This   *+*  limitation of liability shall not apply to liability for death or   *+*  personal injury resulting from such party's negligence to the       *+*  extent applicable law prohibits such limitation. Some               *+*  jurisdictions do not allow the exclusion or limitation of           *+*  incidental or consequential damages, so this exclusion and          *+*  limitation may not apply to You.                                    *+*                                                                      *+************************************************************************++8. Litigation+-------------++Any litigation relating to this License may be brought only in the+courts of a jurisdiction where the defendant maintains its principal+place of business and such litigation shall be governed by laws of that+jurisdiction, without reference to its conflict-of-law provisions.+Nothing in this Section shall prevent a party's ability to bring+cross-claims or counter-claims.++9. Miscellaneous+----------------++This License represents the complete agreement concerning the subject+matter hereof. If any provision of this License is held to be+unenforceable, such provision shall be reformed only to the extent+necessary to make it enforceable. Any law or regulation which provides+that the language of a contract shall be construed against the drafter+shall not be used to construe this License against a Contributor.++10. Versions of the License+---------------------------++10.1. New Versions++Mozilla Foundation is the license steward. Except as provided in Section+10.3, no one other than the license steward has the right to modify or+publish new versions of this License. Each version will be given a+distinguishing version number.++10.2. Effect of New Versions++You may distribute the Covered Software under the terms of the version+of the License under which You originally received the Covered Software,+or under the terms of any subsequent version published by the license+steward.++10.3. Modified Versions++If you create software not governed by this License, and you want to+create a new license for such software, you may create and use a+modified version of this License if you rename the license and remove+any references to the name of the license steward (except to note that+such modified license differs from this License).++10.4. Distributing Source Code Form that is Incompatible With Secondary+Licenses++If You choose to distribute Source Code Form that is Incompatible With+Secondary Licenses under the terms of this version of the License, the+notice described in Exhibit B of this License must be attached.++Exhibit A - Source Code Form License Notice+-------------------------------------------++  This Source Code Form is subject to the terms of the Mozilla Public+  License, v. 2.0. If a copy of the MPL was not distributed with this+  file, You can obtain one at http://mozilla.org/MPL/2.0/.++If it is not possible or desirable to put the notice in a particular+file, then You may include the notice in a location (such as a LICENSE+file in a relevant directory) where a recipient would be likely to look+for such a notice.++You may add additional accurate notices of copyright ownership.++Exhibit B - "Incompatible With Secondary Licenses" Notice+---------------------------------------------------------++  This Source Code Form is "Incompatible With Secondary Licenses", as+  defined by the Mozilla Public License, v. 2.0.
+ README.md view
@@ -0,0 +1,153 @@+# MariaDB Haskell++**Status: Alpha.** Extensive automated tests and benchmarks cover the documented+MariaDB capabilities, but the driver has not yet been validated in real-world+applications. Evaluate it in your own environment; APIs and behavior may change.++A streaming Haskell driver for **MariaDB only**, forked from+[mysql-haskell](https://github.com/winterland1989/mysql-haskell). This fork adds+MariaDB's native statement, session, authentication and replication capabilities,+with bounded decoding and explicit connection ownership. MySQL servers are not+a support target.++**[User guide and examples](https://github.com/aantich/mariadb-haskell/blob/main/docs/USER_GUIDE.md)** ·+**[Live test and benchmark report](https://github.com/aantich/mariadb-haskell/blob/main/validation/RESULTS.md)** ·+**[Benchmark guide](https://github.com/aantich/mariadb-haskell/blob/main/docs/BENCHMARKS.md)** ·+**[Migration guide](https://github.com/aantich/mariadb-haskell/blob/main/docs/MIGRATION.md)**++## Features++| Area | What the driver provides |+| --- | --- |+| Connections | TCP, Unix sockets, verified TLS and client certificates; full Unicode (`utf8mb4`) by default |+| Queries | Streaming list/vector rows, checked text parameters, managed binary prepared statements, multiple results and stored-procedure OUT parameters |+| Native statements | MariaDB binary bulk execution, NULL/DEFAULT/IGNORE indicators, per-unit results, cached and extended column metadata |+| Large data | One-shot streamed byte/text parameters, bounded upload chunks and server cursor fetches |+| Sessions | Public session observations and connection reset with role/schema/charset restoration and an initialization hook for pools |+| Operations | Progress callbacks and opt-in zlib transport compression |+| Authentication | Native password, Unix-socket accounts, ed25519, PARSEC, SHA-2, TLS certificate accounts, explicit PAM dialogs and Kerberos GSSAPI |+| MariaDB values | Signed/unsigned integers, exact decimals, Unicode and bytes, raw zero/partial dates, and wire representations for JSON, UUID, INET, VECTOR and geometry |+| CDC | Row-based binlog decoding, checksums and compressed events, MariaDB GTID resume, complete-transaction checkpoints and consumer-controlled semisync ACKs |+| Reliability | One command owner per connection, bounded messages/metadata/cleanup, stale-handle rejection, cancellation retirement and uncertain-COMMIT diagnostics |++SQL features such as CTEs, window functions, sequences, `RETURNING`, Oracle mode+and system-versioned tables pass through to MariaDB. The driver does not need+a separate API for every server function. This is not an ORM or a collection+of Haskell domain types for every SQL type.++## Quick start++The package is `mariadb-haskell`, starting with version `0.1.0.0`.+Public modules retain `Database.MySQL.*` names, which do not imply MySQL support.+See [release status](https://github.com/aantich/mariadb-haskell/blob/main/docs/RELEASING.md)+for candidate/publication status. Build the checkout as follows; after publication,+add `mariadb-haskell >=0.1 && <0.2` to your application's Cabal dependencies.++On Linux install a C toolchain, zlib development files and MIT Kerberos+(`libkrb5-dev` on Debian/Ubuntu), even when not using GSSAPI. macOS uses the+system GSS framework and Apple's command-line tools. The driver itself does+not require Connector/C.++```sh+cabal update+cabal build lib:mariadb-haskell+```++The complete [Quickstart.hs](https://github.com/aantich/mariadb-haskell/blob/main/examples/Quickstart.hs)+reads connection settings from the environment. For an existing `ConnectInfo`,+a prepared query looks like this:++```haskell+{-# LANGUAGE OverloadedStrings #-}+import Control.Exception (bracket)+import qualified Database.MySQL.Base as DB+import qualified System.IO.Streams as Streams++printCustomer ci customerId =+  bracket (DB.connect ci) DB.close $ \conn ->+    DB.withPreparedStatement conn+      "SELECT name FROM customers WHERE id = ?" $ \statement -> do+        (_, rows) <- DB.queryPrepared statement [DB.MySQLInt64 customerId]+        Streams.foldM (\() row -> print row) () rows+```++Consume every result stream within the connection and statement brackets,+before issuing another command. A connection and its stream have one owning+thread; use separate connections for concurrent workers. Use TLS or a Unix+socket for sensitive credentials. The+[user guide](https://github.com/aantich/mariadb-haskell/blob/main/docs/USER_GUIDE.md)+covers connections, values, transactions, bulk, uploads, cursors, pooling,+authentication, failures and durable CDC consumers.++## Validation and performance++The committed release-input capture from `6bbcf13`, recorded on **9 November+2026**, reports **844 PASS, 0 FAIL, 0 unsupported API inventory entries and 0 unavailable+fixtures** on macOS. The inherited suites cover **183 unit/property cases** (182 pass and one+inherited external-HTTPS case is ignored) and **32 passing integration tests**;+**180 policy tests** pass. A separate Linux profile also passes all 844 checks+and 39 native benchmark smoke paths; its exact revision is retained in the+[release evidence](https://github.com/aantich/mariadb-haskell/blob/main/docs/RELEASING.md#evidence-checkpoint).+This closes the original suite's missing driver APIs+and authentication fixtures; it does not certify every version or platform.++The Mac run measures **63 profiles, seven process repetitions each**. Selected+comparisons use alternatives in the **same current build**:++| Comparison (Current vs Baseline) | Time unit | Baseline | Current | Change vs baseline |+| --- | --- | ---: | ---: | ---: |+| Native bulk vs sequential prepared insert | ns/row | 41,574.20 | 1,966.40 | -95.27% |+| Native bulk vs text batch insert | ns/row | 11,001.40 | 1,966.40 | -82.13% |+| Cached vs uncached wide prepared metadata | ns/execution | 76,824.00 | 56,378.00 | -26.61% |+| Reset/restore vs reconnect/prepare TLS session | ns/request | 38,469,366.67 | 464,833.33 | -98.79% |++Baseline means the named alternative in this build. Change is+`(current / baseline − 1) × 100`; negative means less elapsed time.+The benchmark guide also records allocations. These local measurements are not+promises for other applications.+The host was **not idle** and timing investigations remain open. Streaming+uploads allocate more in the measured case; cursors and compression cost time+on local transfers. Some ordinary workloads remain slower than the original+fork baseline. No allocation-regression signals were found against the frozen+references; this differs from comparing two alternatives within this build.++See the [benchmark guide](https://github.com/aantich/mariadb-haskell/blob/main/docs/BENCHMARKS.md)+for environment, tradeoffs and immutable data. The+**[live report on main](https://github.com/aantich/mariadb-haskell/blob/main/validation/RESULTS.md)**+is refreshed from complete captures, retaining failures, baselines and exact+source provenance. This dated snapshot describes the recorded release inputs;+ordinary documentation/results commits do not claim another measurement.++[![Live validation status](https://raw.githubusercontent.com/aantich/mariadb-haskell/main/validation/results/overview.svg)](https://github.com/aantich/mariadb-haskell/blob/main/validation/RESULTS.md)++## Tested scope and limits++Full recorded profiles use **MariaDB 12.3.3**, macOS arm64/GHC 9.10.3 and+Linux/GHC 9.4.7. Other server/compiler versions need separate profiles.+Capabilities depend on the server version/configuration. Windows is not a+verified release platform and its GSSAPI implementation is unavailable.++CDC fails closed on unsupported non-ignorable events. Advanced XA checkpointing,+two-phase ALTER and storage-engine-binlog GTID semisync are unsupported by the+transaction-aware API. Checkpoints are not persisted by the driver; ACKs alone+do not provide exactly-once destination writes. Text codecs cover UTF-8, ASCII+and MariaDB latin1; other result charsets remain bytes.++Timeouts/cancellation during protocol I/O retire the connection. Reconnect;+never blindly replay a transaction with an uncertain COMMIT. There is no built-in+pool or automatic SQL replay.++## Development and license++Unit/property tests: `cabal test test --enable-tests`. Integration/benchmarks+require a provisioned server first; follow the+[validation instructions](https://github.com/aantich/mariadb-haskell/blob/main/validation/README.md)+and [managed macOS setup](https://github.com/aantich/mariadb-haskell/blob/main/validation/MACOS.md).+Implementation history remains in `docs/`.++Driver code: BSD 3-Clause; see [LICENSE](https://github.com/aantich/mariadb-haskell/blob/main/LICENSE).+Bundled Mozilla CA data: MPL-2.0; see [CA provenance](https://github.com/aantich/mariadb-haskell/blob/main/docs/CA_BUNDLE.md).+Original work by Winterland and upstream contributors retains its copyright+notices. Report reproducible problems through+[GitHub issues](https://github.com/aantich/mariadb-haskell/issues), including+server/compiler versions and sanitized diagnostics.
+ binary-parser-bench/Aeson.hs view
@@ -0,0 +1,361 @@+{-# LANGUAGE BangPatterns, CPP, OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-unused-binds #-}++module Aeson+    (+      aeson+    , aesonLazy+    , value'+    ) where++import Data.ByteString.Builder+  (Builder, byteString, toLazyByteString, charUtf8, word8)++#if !MIN_VERSION_base(4,8,0)+import Control.Applicative ((*>), (<$>), (<*), pure)+import Data.Monoid (mappend, mempty)+#endif++import Control.Applicative (liftA2)+import Control.DeepSeq (NFData(..))+import Control.Monad (forM)+import Data.Attoparsec.ByteString.Char8 (Parser, char, endOfInput, scientific,+                                         skipSpace, string)+import Data.Bits ((.|.), shiftL)+import Data.ByteString (ByteString)+import Data.Char (chr)+import Data.List (sort)+import Data.Scientific (Scientific)+import Data.Text (Text)+import Data.Text.Encoding (decodeUtf8')+import Data.Vector as Vector (Vector, foldl', fromList)+import Data.Word (Word8)+import System.Directory (getDirectoryContents, doesDirectoryExist)+import System.FilePath ((</>), dropExtension)+import qualified Data.Attoparsec.ByteString as A+import qualified Data.Attoparsec.Lazy as L+import qualified Data.Attoparsec.Zepto as Z+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as L+import qualified Data.ByteString.Unsafe as B+import qualified Data.HashMap.Strict as H+import Criterion.Main+import Common (pathTo)++#define BACKSLASH 92+#define CLOSE_CURLY 125+#define CLOSE_SQUARE 93+#define COMMA 44+#define DOUBLE_QUOTE 34+#define OPEN_CURLY 123+#define OPEN_SQUARE 91+#define C_0 48+#define C_9 57+#define C_A 65+#define C_F 70+#define C_a 97+#define C_f 102+#define C_n 110+#define C_t 116++data Result a = Error String+              | Success a+                deriving (Eq, Show)+++-- | A JSON \"object\" (key\/value map).+type Object = H.HashMap Text Value++-- | A JSON \"array\" (sequence).+type Array = Vector Value++-- | A JSON value represented as a Haskell value.+data Value = Object !Object+           | Array !Array+           | String !Text+           | Number !Scientific+           | Bool !Bool+           | Null+             deriving (Eq, Show)++instance NFData Value where+    rnf (Object o) = rnf o+    rnf (Array a)  = Vector.foldl' (\x y -> rnf y `seq` x) () a+    rnf (String s) = rnf s+    rnf (Number n) = rnf n+    rnf (Bool b)   = rnf b+    rnf Null       = ()++-- | Parse a top-level JSON value.  This must be either an object or+-- an array, per RFC 4627.+--+-- The conversion of a parsed value to a Haskell value is deferred+-- until the Haskell value is needed.  This may improve performance if+-- only a subset of the results of conversions are needed, but at a+-- cost in thunk allocation.+json :: Parser Value+json = json_ object_ array_++-- | Parse a top-level JSON value.  This must be either an object or+-- an array, per RFC 4627.+--+-- This is a strict version of 'json' which avoids building up thunks+-- during parsing; it performs all conversions immediately.  Prefer+-- this version if most of the JSON data needs to be accessed.+json' :: Parser Value+json' = json_ object_' array_'++json_ :: Parser Value -> Parser Value -> Parser Value+json_ obj ary = do+  w <- skipSpace *> A.satisfy (\w -> w == OPEN_CURLY || w == OPEN_SQUARE)+  if w == OPEN_CURLY+    then obj+    else ary+{-# INLINE json_ #-}++object_ :: Parser Value+object_ = {-# SCC "object_" #-} Object <$> objectValues jstring value++object_' :: Parser Value+object_' = {-# SCC "object_'" #-} do+  !vals <- objectValues jstring' value'+  return (Object vals)+ where+  jstring' = do+    !s <- jstring+    return s++objectValues :: Parser Text -> Parser Value -> Parser (H.HashMap Text Value)+objectValues str val = do+  skipSpace+  let pair = liftA2 (,) (str <* skipSpace) (char ':' *> skipSpace *> val)+  H.fromList <$> commaSeparated pair CLOSE_CURLY+{-# INLINE objectValues #-}++array_ :: Parser Value+array_ = {-# SCC "array_" #-} Array <$> arrayValues value++array_' :: Parser Value+array_' = {-# SCC "array_'" #-} do+  !vals <- arrayValues value'+  return (Array vals)++commaSeparated :: Parser a -> Word8 -> Parser [a]+commaSeparated item endByte = do+  w <- A.peekWord8'+  if w == endByte+    then A.anyWord8 >> return []+    else loop+  where+    loop = do+      v <- item <* skipSpace+      ch <- A.satisfy $ \w -> w == COMMA || w == endByte+      if ch == COMMA+        then skipSpace >> (v:) <$> loop+        else return [v]+{-# INLINE commaSeparated #-}++arrayValues :: Parser Value -> Parser (Vector Value)+arrayValues val = do+  skipSpace+  Vector.fromList <$> commaSeparated val CLOSE_SQUARE+{-# INLINE arrayValues #-}++-- | Parse any JSON value.  You should usually 'json' in preference to+-- this function, as this function relaxes the object-or-array+-- requirement of RFC 4627.+--+-- In particular, be careful in using this function if you think your+-- code might interoperate with Javascript.  A na&#xef;ve Javascript+-- library that parses JSON data using @eval@ is vulnerable to attack+-- unless the encoded data represents an object or an array.  JSON+-- implementations in other languages conform to that same restriction+-- to preserve interoperability and security.+value :: Parser Value+value = do+  w <- A.peekWord8'+  case w of+    DOUBLE_QUOTE  -> A.anyWord8 *> (String <$> jstring_)+    OPEN_CURLY    -> A.anyWord8 *> object_+    OPEN_SQUARE   -> A.anyWord8 *> array_+    C_f           -> string "false" *> pure (Bool False)+    C_t           -> string "true" *> pure (Bool True)+    C_n           -> string "null" *> pure Null+    _              | w >= 48 && w <= 57 || w == 45+                  -> Number <$> scientific+      | otherwise -> fail "not a valid json value"++-- | Strict version of 'value'. See also 'json''.+value' :: Parser Value+value' = do+  w <- A.peekWord8'+  case w of+    DOUBLE_QUOTE  -> do+                     !s <- A.anyWord8 *> jstring_+                     return (String s)+    OPEN_CURLY    -> A.anyWord8 *> object_'+    OPEN_SQUARE   -> A.anyWord8 *> array_'+    C_f           -> string "false" *> pure (Bool False)+    C_t           -> string "true" *> pure (Bool True)+    C_n           -> string "null" *> pure Null+    _              | w >= 48 && w <= 57 || w == 45+                  -> do+                     !n <- scientific+                     return (Number n)+      | otherwise -> fail "not a valid json value"++-- | Parse a quoted JSON string.+jstring :: Parser Text+jstring = A.word8 DOUBLE_QUOTE *> jstring_++-- | Parse a string without a leading quote.+jstring_ :: Parser Text+jstring_ = {-# SCC "jstring_" #-} do+  s <- A.scan False $ \s c -> if s then Just False+                                   else if c == DOUBLE_QUOTE+                                        then Nothing+                                        else Just (c == BACKSLASH)+  _ <- A.word8 DOUBLE_QUOTE+  s1 <- if BACKSLASH `B.elem` s+        then case Z.parse unescape s of+            Right r  -> return r+            Left err -> fail err+         else return s++  case decodeUtf8' s1 of+      Right r  -> return r+      Left err -> fail $ show err++{-# INLINE jstring_ #-}++unescape :: Z.Parser ByteString+unescape = toByteString <$> go mempty where+  go acc = do+    h <- Z.takeWhile (/=BACKSLASH)+    let rest = do+          start <- Z.take 2+          let !slash = B.unsafeHead start+              !t = B.unsafeIndex start 1+              escape = case B.findIndex (==t) "\"\\/ntbrfu" of+                         Just i -> i+                         _      -> 255+          if slash /= BACKSLASH || escape == 255+            then fail "invalid JSON escape sequence"+            else do+            let cont m = go (acc `mappend` byteString h `mappend` m)+                {-# INLINE cont #-}+            if t /= 117 -- 'u'+              then cont (word8 (B.unsafeIndex mapping escape))+              else do+                   a <- hexQuad+                   if a < 0xd800 || a > 0xdfff+                     then cont (charUtf8 (chr a))+                     else do+                       b <- Z.string "\\u" *> hexQuad+                       if a <= 0xdbff && b >= 0xdc00 && b <= 0xdfff+                         then let !c = ((a - 0xd800) `shiftL` 10) ++                                       (b - 0xdc00) + 0x10000+                              in cont (charUtf8 (chr c))+                         else fail "invalid UTF-16 surrogates"+    done <- Z.atEnd+    if done+      then return (acc `mappend` byteString h)+      else rest+  mapping = "\"\\/\n\t\b\r\f"++hexQuad :: Z.Parser Int+hexQuad = do+  s <- Z.take 4+  let hex n | w >= C_0 && w <= C_9 = w - C_0+            | w >= C_a && w <= C_f = w - 87+            | w >= C_A && w <= C_F = w - 55+            | otherwise          = 255+        where w = fromIntegral $ B.unsafeIndex s n+      a = hex 0; b = hex 1; c = hex 2; d = hex 3+  if (a .|. b .|. c .|. d) /= 255+    then return $! d .|. (c `shiftL` 4) .|. (b `shiftL` 8) .|. (a `shiftL` 12)+    else fail "invalid hex escape"++decodeWith :: Parser Value -> (Value -> Result a) -> L.ByteString -> Maybe a+decodeWith p to s =+    case L.parse p s of+      L.Done _ v -> case to v of+                      Success a -> Just a+                      _         -> Nothing+      _          -> Nothing+{-# INLINE decodeWith #-}++decodeStrictWith :: Parser Value -> (Value -> Result a) -> B.ByteString+                 -> Maybe a+decodeStrictWith p to s =+    case either Error to (A.parseOnly p s) of+      Success a -> Just a+      Error _ -> Nothing+{-# INLINE decodeStrictWith #-}++eitherDecodeWith :: Parser Value -> (Value -> Result a) -> L.ByteString+                 -> Either String a+eitherDecodeWith p to s =+    case L.parse p s of+      L.Done _ v -> case to v of+                      Success a -> Right a+                      Error msg -> Left msg+      L.Fail _ _ msg -> Left msg+{-# INLINE eitherDecodeWith #-}++eitherDecodeStrictWith :: Parser Value -> (Value -> Result a) -> B.ByteString+                       -> Either String a+eitherDecodeStrictWith p to s =+    case either Error to (A.parseOnly p s) of+      Success a -> Right a+      Error msg -> Left msg+{-# INLINE eitherDecodeStrictWith #-}++-- $lazy+--+-- The 'json' and 'value' parsers decouple identification from+-- conversion.  Identification occurs immediately (so that an invalid+-- JSON document can be rejected as early as possible), but conversion+-- to a Haskell value is deferred until that value is needed.+--+-- This decoupling can be time-efficient if only a smallish subset of+-- elements in a JSON value need to be inspected, since the cost of+-- conversion is zero for uninspected elements.  The trade off is an+-- increase in memory usage, due to allocation of thunks for values+-- that have not yet been converted.++-- $strict+--+-- The 'json'' and 'value'' parsers combine identification with+-- conversion.  They consume more CPU cycles up front, but have a+-- smaller memory footprint.++-- | Parse a top-level JSON value followed by optional whitespace and+-- end-of-input.  See also: 'json'.+jsonEOF :: Parser Value+jsonEOF = json <* skipSpace <* endOfInput++-- | Parse a top-level JSON value followed by optional whitespace and+-- end-of-input.  See also: 'json''.+jsonEOF' :: Parser Value+jsonEOF' = json' <* skipSpace <* endOfInput++toByteString :: Builder -> ByteString+toByteString = L.toStrict . toLazyByteString+{-# INLINE toByteString #-}++aeson :: IO [Benchmark]+aeson = do+  path <- pathTo "json-data"+  names <- sort . filter (`notElem` [".", ".."]) <$> getDirectoryContents path+  forM names $ \name -> do+    bs <- B.readFile (path </> name)+    return . bench ("attoparsec/" ++ dropExtension name) $ nf (A.parseOnly jsonEOF') bs++aesonLazy :: IO [Benchmark]+aesonLazy = do+  path <- pathTo "json-data"+  names <- sort . filter (`notElem` [".", ".."]) <$> getDirectoryContents path+  forM names $ \name -> do+    bs <- L.readFile (path </> name)+    return . bench ("attoparsec/lazy-bytestring/" ++ dropExtension name) $ nf (L.parse jsonEOF') bs
+ binary-parser-bench/AesonBP.hs view
@@ -0,0 +1,325 @@+{-# LANGUAGE BangPatterns, CPP, OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-unused-binds #-}++module AesonBP+    (+      aeson+    , aesonLazy+    , value'+    ) where++import Data.ByteString.Builder+  (Builder, byteString, toLazyByteString, charUtf8, word8)++#if !MIN_VERSION_base(4,8,0)+import Control.Applicative ((*>), (<$>), (<*), pure)+import Data.Monoid (mappend, mempty)+#endif++import Control.Applicative (liftA2)+import Control.DeepSeq (NFData(..))+import Control.Monad (forM)+import Data.Bits ((.|.), shiftL)+import Data.ByteString (ByteString)+import Data.Char (chr)+import Data.List (sort)+import Data.Scientific (Scientific)+import Data.Text (Text)+import Data.Text.Encoding (decodeUtf8')+import Data.Vector as Vector (Vector, foldl', fromList)+import Data.Word (Word8)+import System.Directory (getDirectoryContents, doesDirectoryExist)+import System.FilePath ((</>), dropExtension)+import qualified Data.Attoparsec.Zepto as Z+import Data.Binary.Get (Get)+import qualified Data.Binary.Parser as BP+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as L+import qualified Data.ByteString.Unsafe as B+import qualified Data.HashMap.Strict as H+import Criterion.Main+import Common (pathTo)++#define BACKSLASH 92+#define CLOSE_CURLY 125+#define CLOSE_SQUARE 93+#define COMMA 44+#define COLON 58+#define DOUBLE_QUOTE 34+#define OPEN_CURLY 123+#define OPEN_SQUARE 91+#define C_0 48+#define C_9 57+#define C_A 65+#define C_F 70+#define C_a 97+#define C_f 102+#define C_n 110+#define C_t 116++data Result a = Error String+              | Success a+                deriving (Eq, Show)+++-- | A JSON \"object\" (key\/value map).+type Object = H.HashMap Text Value++-- | A JSON \"array\" (sequence).+type Array = Vector Value++-- | A JSON value represented as a Haskell value.+data Value = Object !Object+           | Array !Array+           | String !Text+           | Number !Scientific+           | Bool !Bool+           | Null+             deriving (Eq, Show)++instance NFData Value where+    rnf (Object o) = rnf o+    rnf (Array a)  = Vector.foldl' (\x y -> rnf y `seq` x) () a+    rnf (String s) = rnf s+    rnf (Number n) = rnf n+    rnf (Bool b)   = rnf b+    rnf Null       = ()++-- | Parse a top-level JSON value.  This must be either an object or+-- an array, per RFC 4627.+--+-- The conversion of a parsed value to a Haskell value is deferred+-- until the Haskell value is needed.  This may improve performance if+-- only a subset of the results of conversions are needed, but at a+-- cost in thunk allocation.+json :: Get Value+json = json_ object_ array_++-- | Parse a top-level JSON value.  This must be either an object or+-- an array, per RFC 4627.+--+-- This is a strict version of 'json' which avoids building up thunks+-- during parsing; it performs all conversions immediately.  Prefer+-- this version if most of the JSON data needs to be accessed.+json' :: Get Value+json' = json_ object_' array_'++json_ :: Get Value -> Get Value -> Get Value+json_ obj ary = do+  w <- BP.skipSpaces *> BP.satisfy (\w -> w == OPEN_CURLY || w == OPEN_SQUARE)+  if w == OPEN_CURLY+    then obj+    else ary+{-# INLINE json_ #-}++object_ :: Get Value+object_ = {-# SCC "object_" #-} Object <$> objectValues jstring value++object_' :: Get Value+object_' = {-# SCC "object_'" #-} do+  !vals <- objectValues jstring' value'+  return (Object vals)+ where+  jstring' = do+    !s <- jstring+    return s++objectValues :: Get Text -> Get Value -> Get (H.HashMap Text Value)+objectValues str val = do+  BP.skipSpaces+  let pair = liftA2 (,) (str <* BP.skipSpaces) (BP.word8 COLON *> BP.skipSpaces *> val)+  H.fromList <$> commaSeparated pair CLOSE_CURLY+{-# INLINE objectValues #-}++array_ :: Get Value+array_ = {-# SCC "array_" #-} Array <$> arrayValues value++array_' :: Get Value+array_' = {-# SCC "array_'" #-} do+  !vals <- arrayValues value'+  return (Array vals)++commaSeparated :: Get a -> Word8 -> Get [a]+commaSeparated item endByte = do+  w <- BP.peek+  if w == endByte+    then BP.skipN 1 >> return []+    else loop+  where+    loop = do+      v <- item <* BP.skipSpaces+      ch <- BP.satisfy $ \w -> w == COMMA || w == endByte+      if ch == COMMA+        then BP.skipSpaces >> (v:) <$> loop+        else return [v]+{-# INLINE commaSeparated #-}++arrayValues :: Get Value -> Get (Vector Value)+arrayValues val = do+  BP.skipSpaces+  Vector.fromList <$> commaSeparated val CLOSE_SQUARE+{-# INLINE arrayValues #-}++-- | Parse any JSON value.  You should usually 'json' in preference to+-- this function, as this function relaxes the object-or-array+-- requirement of RFC 4627.+--+-- In particular, be careful in using this function if you think your+-- code might interoperate with Javascript.  A na&#xef;ve Javascript+-- library that parses JSON data using @eval@ is vulnerable to attack+-- unless the encoded data represents an object or an array.  JSON+-- implementations in other languages conform to that same restriction+-- to preserve interoperability and security.+value :: Get Value+value = do+  w <- BP.peek+  case w of+    DOUBLE_QUOTE  -> BP.skipN 1 *> (String <$> jstring_)+    OPEN_CURLY    -> BP.skipN 1 *> object_+    OPEN_SQUARE   -> BP.skipN 1 *> array_+    C_f           -> BP.string "false" *> pure (Bool False)+    C_t           -> BP.string "true" *> pure (Bool True)+    C_n           -> BP.string "null" *> pure Null+    _              | w >= 48 && w <= 57 || w == 45+                  -> Number <$> BP.scientific+      | otherwise -> fail "not a valid json value"++-- | Strict version of 'value'. See also 'json''.+value' :: Get Value+value' = do+  w <- BP.peek+  case w of+    DOUBLE_QUOTE  -> do+                     !s <- BP.skipN 1 *> jstring_+                     return (String s)+    OPEN_CURLY    -> BP.skipN 1 *> object_'+    OPEN_SQUARE   -> BP.skipN 1 *> array_'+    C_f           -> BP.string "false" *> pure (Bool False)+    C_t           -> BP.string "true" *> pure (Bool True)+    C_n           -> BP.string "null" *> pure Null+    _              | w >= 48 && w <= 57 || w == 45+                  -> do+                     !n <- BP.scientific+                     return (Number n)+      | otherwise -> fail "not a valid json value"++-- | Parse a quoted JSON string.+jstring :: Get Text+jstring = BP.word8 DOUBLE_QUOTE *> jstring_++-- | Parse a string without a leading quote.+jstring_ :: Get Text+jstring_ = {-# SCC "jstring_" #-} do+  s <- BP.scan False $ \s c -> if s then Just False+                                   else if c == DOUBLE_QUOTE+                                        then Nothing+                                        else Just (c == BACKSLASH)+  BP.word8 DOUBLE_QUOTE+  s1 <- if BACKSLASH `B.elem` s+        then case Z.parse unescape s of+            Right r  -> return r+            Left err -> fail err+         else return s++  case decodeUtf8' s1 of+      Right r  -> return r+      Left err -> fail $ show err++{-# INLINE jstring_ #-}++unescape :: Z.Parser ByteString+unescape = toByteString <$> go mempty where+  go acc = do+    h <- Z.takeWhile (/=BACKSLASH)+    let rest = do+          start <- Z.take 2+          let !slash = B.unsafeHead start+              !t = B.unsafeIndex start 1+              escape = case B.findIndex (==t) "\"\\/ntbrfu" of+                         Just i -> i+                         _      -> 255+          if slash /= BACKSLASH || escape == 255+            then fail "invalid JSON escape sequence"+            else do+            let cont m = go (acc `mappend` byteString h `mappend` m)+                {-# INLINE cont #-}+            if t /= 117 -- 'u'+              then cont (word8 (B.unsafeIndex mapping escape))+              else do+                   a <- hexQuad+                   if a < 0xd800 || a > 0xdfff+                     then cont (charUtf8 (chr a))+                     else do+                       b <- Z.string "\\u" *> hexQuad+                       if a <= 0xdbff && b >= 0xdc00 && b <= 0xdfff+                         then let !c = ((a - 0xd800) `shiftL` 10) ++                                       (b - 0xdc00) + 0x10000+                              in cont (charUtf8 (chr c))+                         else fail "invalid UTF-16 surrogates"+    done <- Z.atEnd+    if done+      then return (acc `mappend` byteString h)+      else rest+  mapping = "\"\\/\n\t\b\r\f"++hexQuad :: Z.Parser Int+hexQuad = do+  s <- Z.take 4+  let hex n | w >= C_0 && w <= C_9 = w - C_0+            | w >= C_a && w <= C_f = w - 87+            | w >= C_A && w <= C_F = w - 55+            | otherwise          = 255+        where w = fromIntegral $ B.unsafeIndex s n+      a = hex 0; b = hex 1; c = hex 2; d = hex 3+  if (a .|. b .|. c .|. d) /= 255+    then return $! d .|. (c `shiftL` 4) .|. (b `shiftL` 8) .|. (a `shiftL` 12)+    else fail "invalid hex escape"++-- $lazy+--+-- The 'json' and 'value' parsers decouple identification from+-- conversion.  Identification occurs immediately (so that an invalid+-- JSON document can be rejected as early as possible), but conversion+-- to a Haskell value is deferred until that value is needed.+--+-- This decoupling can be time-efficient if only a smallish subset of+-- elements in a JSON value need to be inspected, since the cost of+-- conversion is zero for uninspected elements.  The trade off is an+-- increase in memory usage, due to allocation of thunks for values+-- that have not yet been converted.++-- $strict+--+-- The 'json'' and 'value'' parsers combine identification with+-- conversion.  They consume more CPU cycles up front, but have a+-- smaller memory footprint.++-- | Parse a top-level JSON value followed by optional whitespace and+-- end-of-input.  See also: 'json'.+jsonEOF :: Get Value+jsonEOF = json <* BP.skipSpaces++-- | Parse a top-level JSON value followed by optional whitespace and+-- end-of-input.  See also: 'json''.+jsonEOF' :: Get Value+jsonEOF' = json' <* BP.skipSpaces++toByteString :: Builder -> ByteString+toByteString = L.toStrict . toLazyByteString+{-# INLINE toByteString #-}++aeson :: IO [Benchmark]+aeson = do+  path <- pathTo "json-data"+  names <- sort . filter (`notElem` [".", ".."]) <$> getDirectoryContents path+  forM names $ \name -> do+    bs <- B.readFile (path </> name)+    return . bench ("binary-parser/" ++ dropExtension name) $ nf (BP.parseOnly jsonEOF') bs++aesonLazy :: IO [Benchmark]+aesonLazy = do+  path <- pathTo "json-data"+  names <- sort . filter (`notElem` [".", ".."]) <$> getDirectoryContents path+  forM names $ \name -> do+    bs <- L.readFile (path </> name)+    return . bench ("binary-parser/lazy-bytestring/" ++ dropExtension name) $ nf (BP.parseLazy jsonEOF') bs
+ binary-parser-bench/Bench.hs view
@@ -0,0 +1,26 @@+{-# LANGUAGE OverloadedStrings #-}++module Main (main) where++import Criterion.Main+import qualified Aeson+import qualified AesonBP+import qualified HttpReq+import Data.List++main :: IO ()+main = do+    http <- HttpReq.headers++    putStrLn "start benchmark http request parser"++    defaultMain http++    putStrLn "start benchmark JSON parser"++    aeson <- Aeson.aeson+    aesonbp <- AesonBP.aeson+    aesonLazy <- Aeson.aesonLazy+    aesonbpLazy <- AesonBP.aesonLazy++    (defaultMain . concat . transpose) [ aeson, aesonbp, aesonLazy, aesonbpLazy ]
+ binary-parser-bench/Common.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE CPP #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Common (+      chunksOf+    , pathTo+    , rechunkBS+    , rechunkT+    ) where++import Control.DeepSeq (NFData(rnf))+import System.Directory (doesDirectoryExist)+import System.FilePath ((</>))+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL++#if !MIN_VERSION_bytestring(0,10,0)+import Data.ByteString.Internal (ByteString(..))++instance NFData ByteString where+    rnf (PS _ _ _) = ()+#endif++chunksOf :: Int -> [a] -> [[a]]+chunksOf k = go+  where go xs = case splitAt k xs of+                  ([],_)  -> []+                  (y, ys) -> y : go ys++rechunkBS :: Int -> B.ByteString -> BL.ByteString+rechunkBS n = BL.fromChunks . map B.pack . chunksOf n . B.unpack++rechunkT :: Int -> T.Text -> TL.Text+rechunkT n = TL.fromChunks . map T.pack . chunksOf n . T.unpack++pathTo :: String -> IO FilePath+pathTo wat = do+  exists <- doesDirectoryExist "bench"+  return $ if exists+           then "bench" </> wat+           else wat
+ binary-parser-bench/HttpReq.hs view
@@ -0,0 +1,134 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE OverloadedStrings #-}+module HttpReq (headers) where++import Common (pathTo, rechunkBS)+import Control.Applicative+import Criterion.Main (bench, bgroup, nf, nfIO)+import Control.DeepSeq (NFData(..))+import Criterion.Types (Benchmark)+import Network.Wai.Handler.Warp.RequestHeader (parseHeaderLines)+import Data.ByteString.Internal (c2w, w2c)+import qualified Data.Attoparsec.ByteString as AP+import qualified Data.Attoparsec.ByteString.Char8 as APC+import qualified Data.ByteString.Char8 as BC+import qualified Data.Binary.Parser as BP+import Network.HTTP.Types.Version (HttpVersion, http11)+import qualified Scanner as SC++headers :: IO [Benchmark]+headers = do+  req <- BC.readFile =<< pathTo "http-request.txt"+  return [+        bench "http-req/attoparsec" $ nf (AP.parseOnly attoRequest) req+      , bench "http-req/binary-parsers" $ nf (BP.parseOnly bpRequest) req+      , bench "http-req/scanner" $ nf (SC.scanOnly scRequest) req+      , bench "http-req/warp" $ nfIO (parseHeaderLines (BC.lines req))+      ]++--------------------------------------------------------------------------------++instance NFData HttpVersion where+    rnf !_ = ()++attoHeader = do+  name <- APC.takeWhile1 (APC.inClass "a-zA-Z0-9_-") <* APC.char ':' <* APC.skipSpace+  body <- attoBodyLine+  return (name, body)++attoBodyLine = APC.takeTill (\c -> c == '\r' || c == '\n') <* APC.endOfLine++attoReqLine = do+  m <- (APC.takeTill APC.isSpace <* APC.char ' ')+  (p,q) <- BC.break (=='?') <$> (APC.takeTill APC.isSpace <* APC.char ' ')+  v <- attoHttpVersion+  return (m,p,q,v)++attoHttpVersion = http11 <$ APC.string "HTTP/1.1"++attoRequest = (,) <$> (attoReqLine <* APC.endOfLine) <*> attoManyHeader++attoManyHeader = do+  c <- APC.peekChar'+  if c == '\r' || c == '\n'+    then return []++    else (:) <$> attoHeader <*> attoManyHeader++--------------------------------------------------------------------------------++bpHeader = do+  name <- BP.takeWhile1 isHeaderWord8 <* BP.word8 (c2w ':') <* BP.skipSpaces+  body <- bpBodyLine+  return (name, body)+  where+    isHeaderWord8 w = let c = w2c w in+                   ('a' <= c && c <= 'z')+                || ('A' <= c && c <= 'Z')+                || ('0' <= c && c <= '0')+                || c == '_' || c == '-'++bpBodyLine = BP.takeTill (\w -> w == 13 || w == 10) <* BP.endOfLine++bpReqLine = do+  m <- (BP.takeTill BP.isSpace <* BP.word8 (c2w ' '))+  (p,q) <- BC.break (=='?') <$> (BP.takeTill BP.isSpace <* BP.word8 (c2w ' '))+  v <- bpHttpVersion+  return (m,p,q,v)++bpHttpVersion = http11 <$ BP.string "HTTP/1.1"++bpRequest = (,) <$> (bpReqLine <* BP.endOfLine) <*> bpManyHeader++bpManyHeader = do+  w <- BP.peek+  let c = w2c w+  if c == '\r' || c == '\n'+    then return []+    else (:) <$> bpHeader <*> bpManyHeader++--------------------------------------------------------------------------------++scHeader = do+  name <- takeWhile1 (isHeaderChar . w2c) <* SC.char8 ':' <* SC.skipSpace+  body <- scBodyLine+  return (name, body)+  where+    isHeaderChar c = ('a' <= c && c <= 'z')+                  || ('A' <= c && c <= 'Z')+                  || ('0' <= c && c <= '0')+                  || c == '_' || c == '-'++    takeWhile1 p = do+        bs <- SC.takeWhile p+        if BC.null bs then fail "takeWhile1" else return bs++scEndOfLine = do        -- scanner doesn't provide endOfLine, so we roll one here+    w <- SC.anyWord8+    case w of+        10 -> return ()+        13 -> SC.word8 10+        _  -> fail "endOfLine"+{-# INLINE scEndOfLine #-}++scBodyLine = SC.takeWhile (\w -> let c = w2c w in c /= '\r' && c /= '\n') <* scEndOfLine++scReqLine = do+  m <- (SC.takeWhile (not . BP.isSpace) <* SC.char8 ' ')+  (p,q) <- BC.break (=='?') <$> (SC.takeWhile (not . BP.isSpace) <* SC.char8 ' ')+  v <- scHttpVersion+  return (m,p,q,v)++scHttpVersion = http11 <$ SC.string "HTTP/1.1"++scRequest = (,) <$> (scReqLine <* scEndOfLine) <*> scManyHeader++scManyHeader = do+  w <- SC.lookAhead+  case w of+    Just w' -> do+      let c = w2c w'+      if c == '\r' || c == '\n'+        then return []+        else (:) <$> scHeader <*> scManyHeader+    _ -> fail "scManyHeader"
+ binary-parser-bench/Network/Wai/Handler/Warp/ReadInt.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE MagicHash #-}+{-# LANGUAGE BangPatterns #-}++-- Copyright     : Erik de Castro Lopo <erikd@mega-nerd.com>+-- License       : BSD3++module Network.Wai.Handler.Warp.ReadInt (+    readInt+  , readInt64+  ) where++-- This function lives in its own file because the MagicHash pragma interacts+-- poorly with the CPP pragma.++import Data.ByteString (ByteString)+import qualified Data.ByteString as S+import Data.Int (Int64)+import GHC.Exts (Addr#, Int(..), indexWord8OffAddr#, word2Int#, word8ToWord#)+import GHC.Word++{-# INLINE readInt #-}+readInt :: Integral a => ByteString -> a+readInt bs = fromIntegral $ readInt64 bs++-- This function is used to parse the Content-Length field of HTTP headers and+-- is a performance hot spot. It should only be replaced with something+-- significantly and provably faster.+--+-- It needs to be able work correctly on 32 bit CPUs for file sizes > 2G so we+-- use Int64 here and then make a generic 'readInt' that allows conversion to+-- Int and Integer.++{-# NOINLINE readInt64 #-}+readInt64 :: ByteString -> Int64+readInt64 bs = S.foldl' (\ !i !c -> i * 10 + fromIntegral (mhDigitToInt c)) 0+             $ S.takeWhile isDigit bs++data Table = Table !Addr#++{-# NOINLINE mhDigitToInt #-}+mhDigitToInt :: Word8 -> Int+mhDigitToInt (W8# i) = I# (word2Int# (word8ToWord# (indexWord8OffAddr# addr (word2Int# (word8ToWord# i)))))+  where+    !(Table addr) = table+    table :: Table+    table = Table+        "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\+        \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\+        \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\+        \\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x00\x00\x00\x00\x00\x00\+        \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\+        \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\+        \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\+        \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\+        \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\+        \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\+        \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\+        \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\+        \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\+        \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\+        \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\+        \\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"#++isDigit :: Word8 -> Bool+isDigit w = w >= 48 && w <= 57
+ binary-parser-bench/Network/Wai/Handler/Warp/RequestHeader.hs view
@@ -0,0 +1,172 @@+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE BangPatterns #-}++module Network.Wai.Handler.Warp.RequestHeader (+      parseHeaderLines+    , parseByteRanges+    ) where++import Control.Exception (Exception, throwIO)+import Control.Monad (when)+import Data.Typeable (Typeable)+import qualified Data.ByteString as S+import qualified Data.ByteString.Char8 as B (unpack, readInteger)+import Data.ByteString.Internal (ByteString(..), memchr)+import qualified Data.CaseInsensitive as CI+import Data.Word (Word8)+import Foreign.ForeignPtr (withForeignPtr)+import Foreign.Ptr (Ptr, plusPtr, minusPtr, nullPtr)+import Foreign.Storable (peek)+import qualified Network.HTTP.Types as H+-- import Network.Wai.Handler.Warp.Types+import qualified Network.HTTP.Types.Header as HH+-- $setup+-- >>> :set -XOverloadedStrings++data InvalidRequest = NotEnoughLines [String]+                    | BadFirstLine String+                    | NonHttp+                    | IncompleteHeaders+                    | ConnectionClosedByPeer+                    | OverLargeHeader+                    deriving (Eq, Typeable, Show)++instance Exception InvalidRequest++----------------------------------------------------------------++parseHeaderLines :: [ByteString]+                 -> IO (H.Method+                       ,ByteString  --  Path+                       ,ByteString  --  Path, parsed+                       ,ByteString  --  Query+                       ,H.HttpVersion+                       ,H.RequestHeaders+                       )+parseHeaderLines [] = throwIO $ NotEnoughLines []+parseHeaderLines (firstLine:otherLines) = do+    (method, path', query, httpversion) <- parseRequestLine firstLine+    let path = H.extractPath path'+        hdr = map parseHeader otherLines+    return (method, path', path, query, httpversion, hdr)++----------------------------------------------------------------++-- |+--+-- >>> parseRequestLine "GET / HTTP/1.1"+-- ("GET","/","",HTTP/1.1)+-- >>> parseRequestLine "POST /cgi/search.cgi?key=foo HTTP/1.0"+-- ("POST","/cgi/search.cgi","?key=foo",HTTP/1.0)+-- >>> parseRequestLine "GET "+-- *** Exception: Warp: Invalid first line of request: "GET "+-- >>> parseRequestLine "GET /NotHTTP UNKNOWN/1.1"+-- *** Exception: Warp: Request line specified a non-HTTP request+parseRequestLine :: ByteString+                 -> IO (H.Method+                       ,ByteString -- Path+                       ,ByteString -- Query+                       ,H.HttpVersion)+parseRequestLine requestLine@(PS fptr off len) = withForeignPtr fptr $ \ptr -> do+    when (len < 14) $ throwIO baderr+    let methodptr = ptr `plusPtr` off+        limptr = methodptr `plusPtr` len+        lim0 = fromIntegral len++    pathptr0 <- memchr methodptr 32 lim0 -- ' '+    when (pathptr0 == nullPtr || (limptr `minusPtr` pathptr0) < 11) $+        throwIO baderr+    let pathptr = pathptr0 `plusPtr` 1+        lim1 = fromIntegral (limptr `minusPtr` pathptr0)++    httpptr0 <- memchr pathptr 32 lim1 -- ' '+    when (httpptr0 == nullPtr || (limptr `minusPtr` httpptr0) < 9) $+        throwIO baderr+    let httpptr = httpptr0 `plusPtr` 1+        lim2 = fromIntegral (httpptr0 `minusPtr` pathptr)++    checkHTTP httpptr+    !hv <- httpVersion httpptr+    queryptr <- memchr pathptr 63 lim2 -- '?'++    let !method = bs ptr methodptr pathptr0+        !path+          | queryptr == nullPtr = bs ptr pathptr httpptr0+          | otherwise           = bs ptr pathptr queryptr+        !query+          | queryptr == nullPtr = S.empty+          | otherwise           = bs ptr queryptr httpptr0++    return (method,path,query,hv)+  where+    baderr = BadFirstLine $ B.unpack requestLine+    check :: Ptr Word8 -> Int -> Word8 -> IO ()+    check p n w = do+        w0 <- peek $ p `plusPtr` n+        when (w0 /= w) $ throwIO NonHttp+    checkHTTP httpptr = do+        check httpptr 0 72 -- 'H'+        check httpptr 1 84 -- 'T'+        check httpptr 2 84 -- 'T'+        check httpptr 3 80 -- 'P'+        check httpptr 4 47 -- '/'+        check httpptr 6 46 -- '.'+    httpVersion httpptr = do+        major <- peek $ httpptr `plusPtr` 5+        minor <- peek $ httpptr `plusPtr` 7+        return $ if major == (49 :: Word8) && minor == (49 :: Word8) then+            H.http11+          else+            H.http10+    bs ptr p0 p1 = PS fptr o l+      where+        o = p0 `minusPtr` ptr+        l = p1 `minusPtr` p0++----------------------------------------------------------------++-- |+--+-- >>> parseHeader "Content-Length:47"+-- ("Content-Length","47")+-- >>> parseHeader "Accept-Ranges: bytes"+-- ("Accept-Ranges","bytes")+-- >>> parseHeader "Host:  example.com:8080"+-- ("Host","example.com:8080")+-- >>> parseHeader "NoSemiColon"+-- ("NoSemiColon","")++parseHeader :: ByteString -> H.Header+parseHeader s =+    let (k, rest) = S.break (== 58) s -- ':'+        rest' = S.dropWhile (\c -> c == 32 || c == 9) $ S.drop 1 rest+     in (CI.mk k, rest')++parseByteRanges :: S.ByteString -> Maybe HH.ByteRanges+parseByteRanges bs1 = do+    bs2 <- stripPrefix "bytes=" bs1+    (r, bs3) <- range bs2+    ranges (r:) bs3+  where+    range bs2 =+        case stripPrefix "-" bs2 of+            Just bs3 -> do+                (i, bs4) <- B.readInteger bs3+                Just (HH.ByteRangeSuffix i, bs4)+            Nothing -> do+                (i, bs3) <- B.readInteger bs2+                bs4 <- stripPrefix "-" bs3+                case B.readInteger bs4 of+                    Nothing -> Just (HH.ByteRangeFrom i, bs4)+                    Just (j, bs5) -> Just (HH.ByteRangeFromTo i j, bs5)+    ranges front bs3 =+        case stripPrefix "," bs3 of+            Nothing -> Just (front [])+            Just bs4 -> do+                (r, bs5) <- range bs4+                ranges (front . (r:)) bs5++    stripPrefix x y+        | x `S.isPrefixOf` y = Just (S.drop (S.length x) y)+        | otherwise = Nothing
+ ca-bundle.json view
@@ -0,0 +1,10 @@+{+  "version": "2026-11-05",+  "source": "https://curl.se/ca/cacert-2026-11-05.pem",+  "checksum_source": "https://curl.se/ca/cacert-2026-11-05.pem.sha256",+  "sha256": "427c064216761a1e72a8b30f0d954d08cb6e4435a82091fb865afd20c8497285",+  "certificate_count": 118,+  "license": "MPL-2.0",+  "source_information": "https://curl.se/docs/caextract.html",+  "retrieved_at": "2026-11-09"+}
+ cbits/gssapi.c view
@@ -0,0 +1,109 @@+/* A small ownership boundary around the platform GSSAPI ABI. All calls for one+ * context run on its Haskell worker. Output remains owned by that context until+ * the next step or close; Haskell copies it before making another request. */+#ifdef _WIN32+unsigned int mariadb_gss_live_contexts(void) { return 0; }+#else+#ifdef __APPLE__+#include <GSS/GSS.h>+#else+#include <gssapi/gssapi.h>+#endif+#include <stdlib.h>+#include <stdatomic.h>++/* A credential-free diagnostic also used by cancellation/leak regressions. */+static _Atomic unsigned int live_contexts = 0;+unsigned int mariadb_gss_live_contexts(void) { return atomic_load(&live_contexts); }++struct mariadb_gss_context {+    gss_name_t service;+    gss_ctx_id_t context;+    gss_cred_id_t credentials;+    gss_buffer_desc output;+    gss_OID_desc mechanism;+    int counted;+};++void mariadb_gss_close(struct mariadb_gss_context *state)+{+    OM_uint32 minor;+    if (!state) return;+    if (state->output.value) gss_release_buffer(&minor, &state->output);+    if (state->context != GSS_C_NO_CONTEXT)+        gss_delete_sec_context(&minor, &state->context, GSS_C_NO_BUFFER);+    if (state->credentials != GSS_C_NO_CREDENTIAL)+        gss_release_cred(&minor, &state->credentials);+    if (state->service != GSS_C_NO_NAME)+        gss_release_name(&minor, &state->service);+    if (state->counted) atomic_fetch_sub(&live_contexts, 1);+    free(state);+}++int mariadb_gss_open(const char *principal, size_t length, int negotiate,+                     struct mariadb_gss_context **result,+                     OM_uint32 *major, OM_uint32 *minor)+{+    static unsigned char kerberos[] = {0x2a,0x86,0x48,0x86,0xf7,0x12,0x01,0x02,0x02};+    static unsigned char spnego[] = {0x2b,0x06,0x01,0x05,0x05,0x02};+    struct mariadb_gss_context *state = calloc(1, sizeof(*state));+    gss_buffer_desc name = {length, (void *)principal};+    gss_OID_desc kerberos_mechanism = {sizeof(kerberos), kerberos};+    gss_OID_set_desc desired_mechanisms = {1, &kerberos_mechanism};+    *result = NULL;+    *major = *minor = 0;+    if (!state) return 1;+    state->service = GSS_C_NO_NAME;+    state->context = GSS_C_NO_CONTEXT;+    state->credentials = GSS_C_NO_CREDENTIAL;+    state->mechanism.length = negotiate ? sizeof(spnego) : sizeof(kerberos);+    state->mechanism.elements = negotiate ? spnego : kerberos;+    *major = gss_import_name(minor, &name, GSS_C_NT_USER_NAME, &state->service);+    if (GSS_ERROR(*major)) {+        mariadb_gss_close(state);+        return 2;+    }+    /* Acquire only Kerberos credentials. In particular, a server selecting+     * SPNEGO must not cause the OS to try cached NTLM/other credentials. The+     * explicit credential lease is owned and released with this context. */+    *major = gss_acquire_cred(minor, GSS_C_NO_NAME, 0, &desired_mechanisms,+        GSS_C_INITIATE, &state->credentials, NULL, NULL);+    if (GSS_ERROR(*major)) {+        mariadb_gss_close(state);+        return 2;+    }+    state->counted = 1;+    atomic_fetch_add(&live_contexts, 1);+    *result = state;+    return 0;+}++int mariadb_gss_step(struct mariadb_gss_context *state,+                     const char *input, size_t length,+                     const char **output, size_t *output_length, int *complete,+                     OM_uint32 *major, OM_uint32 *minor)+{+    OM_uint32 flags = 0, ignored;+    gss_buffer_desc token = {length, (void *)input};+    if (state->output.value) gss_release_buffer(&ignored, &state->output);+    state->output.value = NULL;+    state->output.length = 0;+    *output = NULL;+    *output_length = 0;+    *complete = 0;+    /* No delegation: the server receives a service ticket, never a TGT. Mutual+     * authentication binds its reply to the explicitly pinned principal. */+    *major = gss_init_sec_context(minor, state->credentials, &state->context,+        state->service, &state->mechanism, GSS_C_MUTUAL_FLAG, 0,+        GSS_C_NO_CHANNEL_BINDINGS, length ? &token : GSS_C_NO_BUFFER,+        NULL, &state->output, &flags, NULL);+    if (GSS_ERROR(*major)) return 2;+    if (state->output.length > 1048576) return 3;+    *complete = (*major & GSS_S_CONTINUE_NEEDED) == 0;+    if (*complete && !(flags & GSS_C_MUTUAL_FLAG)) return 4;+    *output = state->output.value;+    *output_length = state->output.length;+    return 0;+}++#endif /* !_WIN32 */
+ docs/BENCHMARKS.md view
@@ -0,0 +1,113 @@+# Benchmarks: choosing the right API++This is the **9 November 2026** release-input capture from committed source+`6bbcf13`. [The live report](https://github.com/aantich/mariadb-haskell/blob/main/validation/RESULTS.md)+contains all profiles, failures, allocations, latency percentiles and open+investigations. Values here are not refreshed merely because documentation or+package metadata changes.++## Same-build alternatives++In this table **Baseline** is the named alternative in the *same measured build*,+not upstream mysql-haskell and not a previous release. **Current** is the native+or optional path named in the row. Every elapsed column is a median normalized+by the displayed unit. Change is `(Current / Baseline - 1) × 100`; negative means+less time. Allocation change uses the same comparison and GHC allocation counts.++| Comparison (Current vs Baseline) | Unit | Baseline | Current | Time change vs baseline | Allocation change vs baseline |+| --- | --- | ---: | ---: | ---: | ---: |+| Native bulk vs sequential prepared | ns/row | 41,574.20 | 1,966.40 | -95.27% | -91.12% |+| Native bulk vs text batch | ns/row | 11,001.40 | 1,966.40 | -82.13% | -77.94% |+| Wide prepared metadata cache | ns/execution | 76,824.00 | 56,378.00 | -26.61% | -73.49% |+| TLS reset vs reconnect | ns/request | 38,469,366.67 | 464,833.33 | -98.79% | -77.11% |+| Streamed upload vs buffered | ns/byte | 5.16 | 5.19 | +0.55% | +96.99% |+| Cursor batch 512 vs ordinary stream | ns/row | 372.80 | 858.70 | +130.34% | +4.64% |+| Wide zlib vs plain transport | ns/row | 4,744.00 | 10,539.00 | +122.15% | +42.88% |++Bulk reduces command round trips; wide metadata caching avoids repeated metadata+transfer/decoding. Reuse measures complete equivalent request boundaries,+including reset/restoration or reconnect and statement preparation. The TLS+comparison includes the cost of fresh connection establishment. It is not a+claim that every query becomes 98% faster.++Streaming upload is a retained-memory feature. In this run it allocates about+97% more than buffered upload for the measured 1 MiB payload. The separate+64 MiB fresh-chunk retention workload checks live heap after major GC at four+positions in both warmups and measurement; it is not a throughput comparison+with a buffered source. See raw samples for every checkpoint rather than using+process high-water memory as proof of bounded retention.++Cursors add fetch round trips and server materialization; choose batch size for+resource/control needs. Compression saves wire bytes for compressible data but+adds CPU cost; these local narrow/wide transfers are slower with zlib. There is+no measured WAN speedup claim. Authentication setup profiles include PAM's fresh+TLS/CA construction and GSSAPI's existing service-ticket cache; their timings are+not interchangeable measures of authentication mechanisms.++## Environment and method++* Apple M4 Max, 16 logical CPUs, arm64, macOS 26.4; GHC 9.10.3 with `-O2`,+  `-threaded`, `+RTS -N4 -T`.+* Homebrew MariaDB 12.3.3 on the same machine, UTC, ROW/FULL binlog and CRC32,+  128 MiB InnoDB buffer pool, `innodb_flush_log_at_trx_commit=1`, `sync_binlog=0`.+  These durability settings are part of the profile, not production advice.+* Seven independent processes per profile, two warmups per process, shuffled+  order, value/count/checksum validation. Complete operation latency and+  normalized throughput are different metrics.+* 63 runnable profiles: 24 frozen original/C controls, eight native-statement,+  seven reuse, eight CDC, thirteen wire-feature and three authentication profiles.+  Two frozen historical unsupported placeholders remain as history; actual APIs+  have separate executable measurements.+* The host was **not idle**: Hearthstone and Zoom were active. The isolated+  Linux compiler was paused during Mac measurements. Anton requested that+  validation continue with timing contention explicitly labeled.+  Original-baseline and previous-stage timing investigations remain open,+  including ordinary narrow reads, text writes and setup/helper costs.+  Unchanged C controls also slowed, but contention is not proven to explain+  every difference. These measurements do not establish universal performance+  superiority over upstream or Connector/C.+* Linux correctness uses its own MariaDB 12.3.3/GHC 9.4.7 profile. Its smoke-run+  timings are not compared with Mac benchmarks.++## Evidence and provenance++The retained [capture JSON](https://github.com/aantich/mariadb-haskell/blob/main/validation/results/references/release-0.1.0.0-final-live.json)+contains these numbers under `latest_completed_run.attempt`. The tested source+is commit `6bbcf13fa3b66d96f26c8c4b36f5d69d5f69c25f` with **209 committed executable+inputs**, fingerprint `23135d7aba3c73c1b39691f9b6ae3ea21104c45838d753a47ce9fa6a73892153`. All families preserve+seven repetitions and the matching 149-entry dependency plan. Source/workload+inputs remained stable throughout the run.++The package rename uses a separately recorded metadata adapter around the+byte-for-byte frozen original recorder; see+[release provenance](https://github.com/aantich/mariadb-haskell/blob/main/docs/RELEASING.md#preserve-benchmark-history-through-the-rename).+It changes only the manifest filename/self-package exclusion and entry-point+dispatch. Sampling, workload bodies and comparison policy remain frozen.++All 844 correctness records pass. The complete command exits 1 because timing+gates remain open: **51 ordinary/reference, 0 native, 4 reuse, 0 CDC,+6 wire and 1 authentication timing signals**, with no allocation signals.+These are not silently waived or presented as accepted performance. The original+and each feature baseline, failed/interrupted attempts, and the earlier+[Stage 7 review capture](https://github.com/aantich/mariadb-haskell/blob/d5a005c9e2adf753b5f6b2a4d04763d309159130/validation/results/references/stage7-fixes-full-1-live.json)+remain intact. The final capture follows the committed Alpha metadata, rendered-documentation+corrections and CI linker-profile fix. Every prior full/interrupted capture is+retained; these are source-validation captures, not a selected timing-confirmation window.++The live report also compares **Baseline** (immutable original-driver capture),+**Current** (latest complete capture), and **Change vs baseline** for the frozen+workloads. Each later feature family has its own immutable first capture because+its API did not exist in the original driver. Never compare different workloads,+server settings, compiler/dependency plans or hardware as a driver speedup.++## Reproducing and extending++Use a Git checkout with history and provision a real server **first**, following+[validation/README.md](https://github.com/aantich/mariadb-haskell/blob/main/validation/README.md).+The source tarball contains developer harness sources but is not the complete+historical evidence checkout. The full command is `python3 validation/check.py`;+Python policy checks run with+`python3 -m unittest discover -s validation -p 'test_*.py'`.+Do not silently replace frozen workloads or references. New machine/version/WAN+profiles need separate baselines. Final publication checks require committed+executable inputs, a complete run, and `python3 validation/publish_results.py --check`.
+ docs/CA_BUNDLE.md view
@@ -0,0 +1,36 @@+# Bundled Mozilla trust roots++`MozillaCAStore` uses a static Mozilla CA extract provided by+[curl's CA Extract service](https://curl.se/docs/caextract.html). The current+snapshot is **5 November 2026**, with **118 certificates**. Its exact source URL,+published checksum, retrieval date and certificate count are in `ca-bundle.json`.+The shipped bytes have SHA-256+`427c064216761a1e72a8b30f0d954d08cb6e4435a82091fb865afd20c8497285`.++The extract is distributed under **MPL-2.0**; its notices are preserved in the+PEM file and the full license ships as `LICENSE-MPL-2.0`. The driver's own code+retains its BSD 3-Clause license. The Cabal license expression describes both+components. This is an explicit replacement of the inherited June 2017 bundle.++Runtime connections do not download roots. Package updates carry reviewed+snapshots; OS/custom trust stores remain preferable when applications need a+different or independently managed update policy. The PEM extract does not+contain all Firefox domain/policy constraints, as curl's source page explains.+It is not a copy of every browser's certificate-validation policy.++## Maintenance++Before each release, check the source page for a newer dated extract and review+its published checksum and changes. Update only an explicit dated version:++```sh+python3 tools/update_ca_bundle.py --version 2026-11-05 \+  --sha256 427c064216761a1e72a8b30f0d954d08cb6e4435a82091fb865afd20c8497285+python3 tools/update_ca_bundle.py --check+```++The updater verifies the requested digest against both downloaded bytes and the+source's checksum, then checks all PEM blocks. Commit the bundle and provenance+together. Re-run installed-data checks, verified TLS rejection/success cases and+the complete validation workflow before publication. Do not silently refresh+trust roots during a benchmark or a connection attempt.
+ docs/MIGRATION.md view
@@ -0,0 +1,70 @@+# Migrating from upstream mysql-haskell++This fork supports MariaDB only. Its independent package identity is+`mariadb-haskell`, starting at `0.1.0.0`; replace the upstream `mysql-haskell`+Cabal dependency when migrating.+Keep imports of `Database.MySQL.*`; they are retained intentionally. The+[user guide](https://github.com/aantich/mariadb-haskell/blob/main/docs/USER_GUIDE.md)+has examples for the new APIs.++Depend on the fork rather than both packages:+their module names overlap and their connection/statement types are distinct.+Change the Cabal dependency and rebuild downstream consumers together.++## Behavior to review when upgrading++* **Unicode:** `defaultConnectInfo` now selects `utf8mb4`. `defaultConnectInfoMB4`+  remains an equivalent alias. Explicit narrow client charsets use strict+  encoding; unsupported result charsets remain bytes.+* **Connection ownership:** one command/stream owns a connection at a time.+  Consume on the creating thread and drain before reuse. Concurrent workers+  need separate connections. Raw connection record access bypasses these rules.+* **Timeout/cancellation:** interruption during protocol I/O retires the+  connection. Replace timeout-and-reuse code with discard-and-reconnect.+  There is no automatic SQL retry. Uncertain COMMIT must be reconciled by the+  application rather than retried blindly.+* **Text parameters:** rendering follows observed SQL mode/charset. Unknown+  context is an error, not guessed quoting. Refresh session facts after relevant+  server errors/opaque SQL; prefer prepared bindings. Identifiers are not values.+* **Limits:** logical messages default to 64 MiB, metadata to 16 MiB and+  cleanup/reset to one second. Configure `ConnectionOptions` explicitly for+  workloads that legitimately exceed these budgets; server limits still apply.+* **Values:** generated/affected IDs preserve unsigned 64-bit width. Raw zero+  and partial dates are represented explicitly. `Field` conversions require+  matching fixed-width constructors; applications must handle decode failures.+* **Result shape:** use multi-result APIs for procedures and OUT parameters.+  Single-result APIs drain and report `ExtraResultSets`; multi-result APIs+  buffer rows, so they are not an unbounded streaming export facility.+* **Prepared handles:** prefer `withPreparedStatement` and managed APIs over+  raw statement IDs. Close/reset/reprepare and connection reset have explicit+  generation/cache lifetimes. Reset invalidates every existing handle.+* **Transactions:** `withTransaction` is not nested. DDL can implicitly commit.+  Bounded cleanup preserves the primary exception and records secondary failure.+* **TLS/build:** Linux builds need MIT Kerberos development libraries in addition+  to zlib/C tooling. macOS uses GSS.framework. Use a managed system/custom CA+  store; the bundled Mozilla snapshot now has dated provenance and its own MPL-2.0 notice.+* **External authentication:** PAM and Kerberos are explicit opt-ins. PAM needs+  TLS/socket transport; Kerberos needs an exact service principal, existing+  credentials and `-threaded`. Windows GSSAPI is unavailable.+* **CDC:** prefer `openCDC`, complete-group checkpoints and manual ACKs after+  durable destination/checkpoint storage. Legacy on-read ACKs mean receipt only.+  Unsupported non-ignorable events fail closed. Persist coordinates/GTIDs,+  not opaque checkpoint `Show` output.++* **Diagnostics:** `Show ConnectInfo` now redacts password bytes, including empty+  passwords, while retaining endpoint/account context. Authentication bytes are unchanged.+* **Developer tooling:** the Hackage package builds the library by default.+  The validation executable requires `-fvalidation`; the repository's own+  `cabal.project` opts in, while source archives omit that development project.++## New APIs to adopt deliberately++Native bulk and metadata caching reduce repeated command/metadata costs.+Session reset supports clean external-pool reuse. Streamed parameters and+cursors give resource control; progress and compression are opt-in. Their+performance tradeoffs are described in the+[benchmark guide](https://github.com/aantich/mariadb-haskell/blob/main/docs/BENCHMARKS.md).++The full tested server profile is MariaDB 12.3.3 on macOS/Linux. Wider server,+compiler and OS support needs additional validation; inherited dependency+bounds or exposed protocol modules do not establish that support.
+ docs/RELEASING.md view
@@ -0,0 +1,198 @@+# Releasing mariadb-haskell++Release preparation, 9 November 2026. Package **mariadb-haskell 0.1.0.0**;+maintainer **Anton Antich <anton.antich@gmail.com>**. Public imports remain+`Database.MySQL.*`. No public Hackage release has been uploaded.++**Readiness: Alpha.** Automated tests and benchmarks do not replace real-world+application validation, which is still outstanding. The Cabal stability field,+description, README, user guide and main API documentation identify this status.++**Publication gate:** upload a candidate and its documentation for Anton to review+as rendered on Hackage. Do not publish to the main Hackage index until Anton has+reviewed that preview and explicitly approved publication. Candidate upload and+documentation inspection do not grant permission to publish the release.++## Implemented release work++* Independent Cabal manifest, self-dependencies, generated `Paths` module,+  examples, Linux tooling and Nix attributes now use `mariadb-haskell`.+* The minimum supported compiler is GHC 9.4 (`base >=4.17`); the current upper+  bound admits GHC 9.12. `tested-with` lists the actually recorded 9.4.7/9.10.3+  profiles, not every compiler listed in the broader CI matrix.+* Linux CI installs MIT Kerberos headers/libraries alongside zlib/C tooling.+  Nix explicitly resolves `gssapi_krb5` to `libkrb5`. Native macOS uses GSS.framework.+  Generic CI covers Linux/macOS; Windows is outside the verified release scope.+  The GHC 9.4 Linux job uses Ubuntu 24.04: its bindist selects Gold, which+  was missing from the Ubuntu 26.04 runner in the first CI attempt.+* The validation executable is a **manual, default-off** package flag. Only+  the developer checkout's `cabal.project` opts in and selects optimization 2.+  Source archives omit that project. A normal Hackage library installation+  does not install a benchmark/validation command.+* The old June 2017 CA file is replaced by the verified 5 November 2026 Mozilla+  extract: 118 certificates, exact source/checksum/provenance and MPL-2.0 notice.+  See [CA bundle maintenance](https://github.com/aantich/mariadb-haskell/blob/main/docs/CA_BUNDLE.md).+* `Show ConnectInfo` now redacts passwords without evaluating them. New unit+  regressions cover empty/text/binary secrets, preserved credentials, context+  and Show precedence.+* README, user/migration/benchmark guides and examples ship in the source+  archive. Public application API comments explain ownership, lifetimes,+  limits, cancellation, reset, authentication and CDC durability.+* `cabal check` is clean. Optimization is selected by the developer project or+  explicit benchmark command rather than forced through published component flags.++## Preserve benchmark history through the rename++`validation/run.py` remains byte-for-byte frozen, SHA-256+`e756bc0f593b54bc9207fd88d2a53120a4654832650d0a148555a69ebd413346`.+The command entry point is now `validation/release_recorder.py`. It first verifies+that digest, substitutes only the manifest filename and self-package dependency+exclusion, and handles entry-point dispatch itself. Workload bodies, warmups,+repetitions, ordering, comparisons and frozen references remain unchanged.++Every new recorder snapshot separately records the original hash, adapter hash,+derived body hash and exact substitutions. The publication guard verifies this+against current fingerprinted inputs. Tests reject any change to the pinned+recorder, including sampling or regression thresholds. Matching original workload+hashes do not assert that the metadata adapter existed in the old capture.+All immutable original/feature baselines and previous measurements remain intact.++## Source package and documentation verification++Run from a checkout, using a new output directory each time:++```sh+python3 -m unittest discover -s validation -p 'test_*.py'+python3 tools/update_ca_bundle.py --check+python3 tools/check_release.py --build --output /tmp/mariadb-release-check+```++The checker creates the actual source archive, rejects private/development+files, verifies required source/data/license/doc files, and unpacks a fresh+package without Git or the development project. It builds the library/tests,+copies runtime data to Cabal's configured prefix, runs the existing unit/property+suite, compiles both examples and builds Hackage-format Haddock. It retains+per-command logs, `checks.json`, source/docs archives and `SHA256SUMS`.+Its build uses only the archive; historical full validation still needs the+Git/evidence checkout and a provisioned MariaDB server.++CI repeats source-archive checks on Linux and macOS, with broader compiler and+crypton-series builds separately. Configuring a job is not proof that a remote+job ran; the release evidence below distinguishes local results from pending CI.++## Final validation and measurements++Install/start/provision MariaDB first, following+[validation](https://github.com/aantich/mariadb-haskell/blob/main/validation/README.md).+Commit the final executable inputs locally on main, then run:++```sh+python3 validation/check.py+python3 validation/publish_results.py --check+```++Retain raw complete evidence and update the live report, JSON and colored+summary together. Never relabel old review captures as new committed-source+measurements or clear their dirty-input history. Deferred timing investigations+remain visible; support/performance claims stay limited to measured environments.+Results-only and ordinary documentation updates do not require an endless rerun.++## Candidate inspection and publication++Preview: [Hackage candidate](https://hackage.haskell.org/package/mariadb-haskell-0.1.0.0/candidate)+· [Rendered application API](https://hackage.haskell.org/package/mariadb-haskell-0.1.0.0/candidate/docs/Database-MySQL-Base.html).+These are candidate previews, not a public-index release. Anton's documentation+review and explicit publication approval are still required.+++The public package lookup returned HTTP 404 during preparation; this is not a+name reservation. Anton's locally configured token was verified by a read-only Hackage request+as account **aantich**, and uploader-group membership is confirmed. The+configuration is owner-only and its Hackage repository uses HTTPS.+Do not put passwords/tokens in source, logs or this chat.++For local access, sign in to Hackage's+[account-management page](https://hackage.haskell.org/users/account-management)+and create an authentication token. `cabal path --config-file` identifies the+active configuration (this Mac uses `/Users/aantich/.cabal/config`). Add an+unindented `token: YOUR_TOKEN` there using a local editor and restrict the+file to its owner (`chmod 600`). Cabal 3.16.1 supports this global token field.+Never pass the actual token through a committed script or chat. New accounts+require uploader approval; Hackage's+[registration page](https://hackage.haskell.org/users/register-request) explains+endorsements and trustee requests. Candidate access is not guaranteed merely by+setting the public maintainer email.++After technical checks pass, upload a **candidate** using the exact checksummed+artifacts (without `--publish`):++```sh+cabal upload /path/to/mariadb-haskell-0.1.0.0.tar.gz+cabal upload --documentation /path/to/mariadb-haskell-0.1.0.0-docs.tar.gz+```++Inspect the candidate README, changelog, maintainer, license notices, user guide,+examples and rendered Haddock; verify source/API links and the native dependency+explanation. Candidate inspection requires upload access and is separate from+local artifact verification. Public publication is the final deliberate action;+[Hackage's upload policy](https://hackage.haskell.org/upload) explains why candidate+review matters. See the [Cabal command reference](https://cabal.readthedocs.io/en/stable/cabal-commands.html)+for authentication/upload options.++## Supported scope++The original capability inventory is green. Advanced XA checkpointing,+two-phase ALTER/storage-engine-binlog GTID semisync, broader text codecs and+untested server/OS profiles remain explicitly excluded. A scoped first release+can retain these limits. Exposed low-level connection/protocol helpers remain+experimental implementation APIs, with some documentation/link debt; the main+application API has complete Haddock item coverage. Retaining module names+is compatibility, not a MySQL server support promise.++## Evidence checkpoint++Committed release inputs: `6bbcf13fa3b66d96f26c8c4b36f5d69d5f69c25f`,+209 input files, SHA-256+`23135d7aba3c73c1b39691f9b6ae3ea21104c45838d753a47ce9fa6a73892153`.++* Mac correctness: **844 PASS / 0 FAIL / 0 unsupported / 0 unavailable**.+  Inherited suites: **183 unit/property cases** (182 pass, one inherited+  external-HTTPS ignore) and **32 passing integration tests**.+* **180 policy tests pass**. The archive rejection test was tightened to match+  its intended error reason; the interrupted earlier capture remains retained.+* Full Mac measurements: **63 runnable profiles × seven repetitions**,+  including all five additive families. Frozen workloads/references and the+  149-entry dependency plan remain unchanged. The strict publication guard passes.+* The full command exits 1 because performance acceptance remains open:+  51 ordinary/reference, 0 native, 4 reuse, 0 CDC, 6 wire and+  1 authentication timing signals; no+  allocation signals. Hearthstone/Zoom were active, and Anton explicitly requested+  continuation with timing contention labeled. Linux compilation was paused+  during the Mac capture. The pre-Alpha run is retained; this final run follows the newly committed+  Alpha metadata, rendered-documentation and CI corrections; every earlier+  capture remains retained rather than selecting better timings.+* Both Nix CI derivations instantiate on isolated Linux; dummy credential/build+  directories are absent from the filtered source. This is **evaluation**, not+  a completed Nix VM test locally. The subsequent+  [GitHub Nix job](https://github.com/aantich/mariadb-haskell/actions/runs/42645834954/job/129258710100)+  passed the package build and all 32 MariaDB VM integration cases. The broader+  compiler/OS jobs are tracked separately in the receipt.+* The independent archive check passes all 12 steps, including installed-CA+  lookup outside Cabal, 183 unit/property cases, both examples and Hackage-format Haddock.+  Exact artifact checksums and archive/inspection outcomes are retained in the+  [release receipt](../validation/results/release-0.1.0.0.json).+* Main application API Haddock item coverage is complete; all 117 authored local+  API links across the six main pages resolve. Generated instance links and+  experimental low-level helpers retain some documentation/link debt.++The Linux profile at `250ad82` passes all 844 correctness records, 180 policy+tests and 39 native benchmark smoke paths. Its raw revision is retained; the+subsequent Alpha change is readiness metadata and API documentation. Linux+smoke timings are never compared with Mac baselines.++The committed Alpha captures pass correctness/evidence gates. The preview+inspection found legacy dot separators in Cabal's description and two slash-heavy+Haddock phrases; these documentation-only corrections are committed separately.+The final source/docs archive identities and remote CI state are recorded in the+release receipt. Automated coverage has not established real-world application+readiness. **Public release awaits Anton's documentation review and explicit approval.**
+ docs/USER_GUIDE.md view
@@ -0,0 +1,351 @@+# MariaDB Haskell user guide++**Alpha software:** automated validation is extensive, but real-world application+validation is still outstanding. Evaluate the driver with your own workloads;+APIs and behavior may change. The documented test scope is not a production+readiness claim.++Start with `Database.MySQL.Base`; add `Database.MySQL.TLS`,+`Database.MySQL.Field` and `Database.MySQL.BinLog` as needed. The historical+`MySQL` names are API/wire identifiers, not a server support claim.++[Quickstart.hs](https://github.com/aantich/mariadb-haskell/blob/main/examples/Quickstart.hs)+is a complete program.+[GuideExamples.hs](https://github.com/aantich/mariadb-haskell/blob/main/examples/GuideExamples.hs)+contains compilable helpers for the APIs below. Both ship in the source tarball.+Snippets use `OverloadedStrings`, `DB` for `Database.MySQL.Base`, `TLS` for+`Database.MySQL.TLS`, `Streams` for `System.IO.Streams`, and `bracket` from+`Control.Exception`. `ci`, credentials and handlers come from your application.++* [Connections and TLS](#installation-and-connections)+* [Streaming, parameters and values](#streaming-rows-binding-and-values)+* [Results and transactions](#multiple-results-and-transactions)+* [Bulk and metadata](#native-bulk-and-cached-metadata)+* [Uploads, cursors and optional wire features](#uploads-cursors-progress-and-compression)+* [Sessions and pools](#session-observations-and-pool-reuse)+* [PAM and Kerberos](#pam-and-kerberos-authentication)+* [Durable CDC](#cdc-and-durable-resume)+* [Failures and budgets](#errors-and-budgets)++## Installation and connections++The package is `mariadb-haskell`, version `0.1.0.0`; public modules remain+`Database.MySQL.*`. Add `mariadb-haskell >=0.1 && <0.2` to Cabal dependencies once+the release is published, or build this checkout with `cabal build lib:mariadb-haskell`.+The [release status](https://github.com/aantich/mariadb-haskell/blob/main/docs/RELEASING.md)+distinguishes prepared artifacts/candidates from public publication.++Linux builds need a C compiler, zlib and MIT Kerberos headers/libraries+(`build-essential zlib1g-dev libkrb5-dev` on Debian/Ubuntu), even without GSSAPI+use. macOS uses Apple's GSS framework and command-line tools. The driver does+not link Connector/C; validation's independent C controls do. Build GSSAPI+applications with `-threaded`. Full profiles use GHC 9.10.3 on Mac and 9.4.7 on+Linux; the broader dependency bounds are not a tested compiler matrix.++```haskell+let ci = DB.defaultConnectInfo+      { DB.ciHost = "127.0.0.1"+      , DB.ciPort = 3306+      , DB.ciUser = "application"+      , DB.ciPassword = passwordFromConfiguration+      , DB.ciDatabase = "application"+      }+bracket (DB.connect ci) DB.close $ \conn -> do+  (_, rows) <- DB.query_ conn "SELECT VERSION(), CURRENT_USER()"+  Streams.foldM (\() row -> print row) () rows+```++Load secrets from configuration rather than source code or logs.+`Show ConnectInfo` redacts the password; keep other configuration and SQL diagnostics appropriately scoped.+`defaultConnectInfo` uses full Unicode `utf8mb4`; `defaultConnectInfoMB4` is an+equivalent compatibility name. TCP does not automatically enable TLS.++For verified TLS, build parameters once and reuse them when opening connections.+The tuple's name is the certificate DNS name, which can differ from `ciHost`:++```haskell+params <- TLS.makeClientParams (TLS.CustomCAStore "/etc/application/mariadb-ca.pem")+bracket (TLS.connect ci (params, "db.example.com")) DB.close useConnection+```++`SystemCAStore` reads OS trust roots; `MozillaCAStore` uses the installed bundled+snapshot, currently **5 November 2026**. Its source, checksum, separate MPL-2.0+license and update policy are in+[CA provenance](https://github.com/aantich/mariadb-haskell/blob/main/docs/CA_BUNDLE.md).+It is not automatically refreshed with the OS. Missing/malformed CA files fail+at parameter construction. `makeClientParams'` also loads a+client certificate, chain and private key for X.509-required accounts. Retain+peer/hostname verification. Repository fixture keys/certificates are public+test material, never production credentials.++For local sockets use `DB.connectUnixSocket "/path/to/mariadb.sock" ci`.+Unix-socket authentication maps the OS identity to a configured MariaDB account;+a SQL password does not change that OS identity.++## Streaming rows, binding and values++`query_` sends literal SQL. `query` renders checked text parameters using the+session's SQL mode/charset. Prefer managed prepared bindings for repeated+queries and untrusted data:++```haskell+DB.withPreparedStatement conn+  "SELECT id, name FROM customers WHERE id >= ? ORDER BY id" $ \statement -> do+    (_, rows) <- DB.queryPrepared statement [DB.MySQLInt64 minimumId]+    Streams.foldM (\() row -> handleRow row) () rows+```++Placeholders bind **values**, not identifiers, keywords or arbitrary SQL+fragments. Raw `Query`/`_` APIs leave quoting to the caller. Never concatenate+untrusted input into SQL.++Queries stream rows rather than collect the whole result; one large field still+occupies memory up to the message budget. Read on the creating thread, inside+the connection/statement lifetime, and consume to EOF before another command.+Use `Streams.skipToEof` for intentional discard. An unfinished ordinary stream+prevents another command. A statement bracket attempts bounded cleanup and+retires the connection if synchronization cannot be restored. Do not return a+live stream from its bracket. Use separate connections for concurrent workers.++The examples use `Streams.foldM` to consume through EOF. io-streams' `mapM_`+attaches effects to a returned stream; calling it without consuming that stream+does not run the whole row consumer.++`MySQLValue` preserves width/signedness: `MySQLInt64`, `MySQLInt64U`,+`MySQLDecimal`, `MySQLText`, `MySQLBytes`, `MySQLNull`, etc.+`Database.MySQL.Field.fromField` returns `Either DecodeError a`. Fixed-width+instances require a matching constructor; they do not silently narrow or parse+arbitrary text. Use `Maybe a` for nullable columns, `Scientific` for exact+decimals and explicit fixed-width integer types. There are no machine-width+`Int`/`Word` instances. After checking row shape, for example:++```haskell+case (fromField value :: Either DecodeError Int64) of+  Left problem -> reportSchemaMismatch problem+  Right number -> useNumber number+```++JSON, UUID, INET4/INET6, VECTOR and GEOMETRY are tested through wire+representations/SQL conversions; the package does not supply a Haskell domain+type for every SQL type. NULL differs from empty text/bytes. Zero/partial dates+have raw constructors with `DateParts`; handle these explicitly. SQL TIME can+be negative or exceed 24 hours, so cannot always become `TimeOfDay`.++Text codecs strictly handle UTF-8, ASCII and MariaDB latin1 (CP1252 mapping).+Other result charsets remain bytes for application decoding. Unrepresentable+text parameters fail rather than replace characters. `preparedMetadata` reads+owned metadata without SQL, including negotiated extended attributes.++## Multiple results and transactions++`execute_`/`executePrepared` return `OK`, including affected rows and the full+64-bit insert ID. Single-result APIs reject unexpected/extra row results with+`ExtraResultSets` after draining. Use `queryMulti_`/`queryMulti` for every text+result or `queryPreparedMulti` for all prepared results and OUT parameters.+`StatementResult` distinguishes OK/rows; `PreparedResult` also distinguishes OUT+parameters. These multi-result APIs **buffer all rows**: use bounded procedure+results rather than unbounded exports.++```haskell+DB.withTransaction conn $+  DB.withPreparedStatement conn+    "UPDATE accounts SET balance = balance - ? WHERE id = ?" $ \statement -> do+      _ <- DB.executePrepared statement [DB.MySQLInt64 amount, DB.MySQLInt64 accountId]+      pure ()+```++`withTransaction` commits on success and attempts bounded rollback on failure.+It does not implement nested transactions/savepoints. Consume results inside+the action. DDL can implicitly commit; the helper cannot make such statements+transactional. Use transactional tables where required.++If COMMIT was sent but its reply was lost, inspect `connectionFailure` and+`commitOutcomeUncertain`. Reconcile using your application's transaction+identity rather than replaying writes automatically. The original exception+survives rollback/cleanup failure; secondary cleanup diagnostics are recorded+on the connection.++## Native bulk and cached metadata++`executeBulk` uses MariaDB's native binary bulk command, avoiding a round trip+per row:++```haskell+DB.withPreparedStatement conn+  "INSERT INTO measurements(sensor_id, reading) VALUES (?, ?)" $ \statement -> do+    result <- DB.executeBulk statement+      [ [DB.BulkValue (DB.MySQLInt64 1), DB.BulkValue (DB.MySQLDouble 12.5)]+      , [DB.BulkValue (DB.MySQLInt64 2), DB.BulkValue DB.MySQLNull]+      ]+    print result+```++Each row must match arity. Each parameter column uses one compatible concrete+wire type/signedness. `BulkValue MySQLNull` is NULL; `BulkDefault` requests the+server default; `BulkIgnore` supplies MariaDB's ignore indicator. MariaDB decides+indicator legality for the statement. `executeBulkUnitResults` returns per-unit+IDs/affected counts with extra response/allocation costs.++The default row budget is 10,000 and message limits also apply. Oversized or+mixed-type batches are rejected, never silently split. Chunk input explicitly;+wrap chunks in a transaction if all-or-nothing behavior is required. Server+constraints and storage-engine semantics still apply.++Prepared metadata caching defaults on with a connection-wide budget, subject+to server negotiation. DDL/reprepare, close and reset refresh/invalidate it.+Never keep handles after reset or use them on a different connection.++## Uploads, cursors, progress and compression++```haskell+DB.withPreparedStatement conn "INSERT INTO files(contents) VALUES (?)" $ \statement -> do+  _ <- DB.executePreparedStream statement [DB.PreparedBytes byteSource]+  pure ()+```++`byteSource :: InputStream ByteString` is consumed once. `PreparedText` takes+`InputStream Text`; `PreparedValue` supplies scalars. Empty streams mean empty+non-NULL values. Bracket input files around the whole execution. Produce bounded+fresh chunks rather than retaining the source in a list. Upload chunks are at+most 64 KiB; the default cumulative execution budget is 64 MiB. Source failures,+encoding errors or cancellation after upload starts retire the connection.+MariaDB can defer upload errors until EXECUTE. Streaming reduces retained input+memory, not necessarily allocation or elapsed time.++For controlled fetches, nest `withCursor` inside `withPreparedStatement`, then+repeatedly `fetchCursor cursor batchSize`. Fully consume each batch before+checking `cursorExhausted` or fetching again. Sizes must be positive and at most+the configured maximum (65,536 by default). Cursors may materialize results on+the server and add round trips; ordinary row streams are the throughput starting+point. See the complete cursor helper in `GuideExamples.hs`.++Set `enableProgressReporting = True` in `ConnectionOptions` before connecting,+then `setProgressCallback conn (Just callback)`. Callbacks run synchronously+while consuming supported server operations' replies. Keep them quick and do+not reenter the same connection. Callback failure/cancellation retires it.++Set `enableCompression = True` before connecting for negotiated zlib transport+compression. It composes with TLS and other supported features. The default+threshold is 1 KiB and raw frames are used when compression saves no bytes.+Compression costs CPU and makes the measured local workloads slower; measure+your real data/network first.++## Session observations and pool reuse++`getSessionState` issues no SQL and returns schema/variable/status/transaction+observations, a change number and the last received changes. Unknown facts+remain explicit. After ERR/opaque context-changing SQL, explicitly+`refreshSessionState` before text rendering when needed; binary binding avoids+SQL quoting but still needs correct text encoding.++`resetConnection` resets server state and restores the constructor's database,+charset/collation and original active role. It restores server session defaults,+not every custom setting. `resetConnectionWith conn initialize` reapplies your+settings under the same exclusive lease and cleanup deadline. The initializer+must consume all results and leave no active transaction.++There is no built-in pool. A pool must ensure exclusive checkout, reject broken+connections, reset between requests, and retain no live streams/cursors/handles.+Reset invalidates every prepared handle. Use an explicit constructor database:+MariaDB cannot restore a selected database to "none". Reset needs server 10.2.4++(the full tested profile is 12.3.3). Failed/cancelled reset discards the connection.++## PAM and Kerberos authentication++Native password, ed25519, PARSEC and SHA-2 negotiate according to the account's+server plugin/transport. Certificates and Unix-socket identity also need matching+server configuration. The driver does not install plugins or configure accounts.+External credentials require explicit `AuthenticationOptions`:++```haskell+let auth = DB.defaultAuthenticationOptions+      { DB.authenticationDialog = Just (DB.passwordDialog pamPassword) }+bracket (TLS.connectWithAuthentication DB.defaultConnectionOptions auth ci+          (params, "db.example.com")) DB.close useConnection+```++PAM replies require TLS or a Unix socket. `passwordDialog` answers only the first+hidden prompt. For usernames/PINs/multiple factors inspect `dialogEcho`,+`dialogRound`, `dialogLast` and `dialogText` in your callback. Prompts are untrusted+server bytes, not trusted terminal control text. No terminal fallback occurs.+Exceptions propagate and close the transport. Dialogs are bounded to 16 exchanges+and 65,535-byte prompts/replies; replies cannot contain NUL.++For Kerberos use+`authenticationGSSAPIPrincipal = Just "mariadb/db.example.com@EXAMPLE.COM"`.+The service principal is exact, including realm. Configure credentials/ticket+cache before connecting and build with `-threaded`. GSSAPI does not use the SQL+password; pinning the principal requires GSSAPI rather than password-only success.+There is no NTLM/SSPI or credential delegation. Login does not encrypt SQL:+use TLS when required. A native provider call can finish after cancellation+under OS/Kerberos policy, while the deadline retires the client transport.+Windows GSSAPI is unavailable.++## CDC and durable resume++Use `Database.MySQL.BinLog` (qualified as `CDC`), a dedicated connection, a+unique nonzero replica server ID, row binlogging and the replication permissions+required by the server. Check binlog retention and grants before connecting.+A CDC connection cannot issue ordinary queries.++```haskell+gtids <- either (ioError . userError) pure (CDC.parseGtidSet savedGtidBytes)+let options = (CDC.defaultCDCOptions (CDC.FromGtid gtids))+      { CDC.cdcAcknowledgements = CDC.AcknowledgeManually }+bracket (CDC.openCDC conn replicaId options) CDC.cdcClose consumeCDC+```++MariaDB GTIDs are `domain-server-sequence`, not UUID intervals. Persist+`renderGtidSet` from `checkpointGtids` for GTID starts, or `checkpointPosition`'s+file/next position for file starts. Reconstruct `FromGtid`/`FromBinLog` from those+durable coordinates on a fresh connection. Do not persist opaque tokens using+`Show`. `resumeFromCheckpoint` helps with an in-memory token; purged history is+an error, never a silent fallback.++`cdcLatestCheckpoint` is a **decoded complete-group boundary**, not a destination+commit. Consume/apply every event through it, atomically persist destination+changes and resume coordinates, then `cdcAcknowledge` its active-stream token.+The driver persists nothing and does not promise exactly-once delivery. Manual+mode requires server semisync. ACKs release receipt waits, not application+distributed transactions. Disabled ACK mode is the default. Legacy `dumpBinLog`+on-read ACKs mean receipt only and do not prove downstream durability.++The pull-driven decoder retains bounded table metadata, not whole transactions.+Consumers must avoid unbounded transaction accumulation. Future, foreign,+incomplete and backward tokens are rejected. EOF/failure/close ends the connection;+reopen and resume from your last durable coordinates. Advanced XA checkpointing,+two-phase ALTER and storage-engine-binlog GTID semisync fail closed. Other+unknown non-ignorable events are rejected too. File dump starts have a 32-bit+wire position limit even though observed/checkpoint positions are 64-bit.++## Errors and budgets++Keep the original exception; inspect `connectionStatus`/`connectionFailure` for+lifecycle and secondary cleanup diagnostics. A complete `ERRException` normally+preserves wire synchronization while invalidating session facts. Malformed+replies, transport failure and cancellation during I/O retire the connection.+Ownership, stale-handle and parameter errors may fail locally before sending.++| Default budget | Value |+| --- | ---: |+| Establishment deadline | 10 seconds |+| Cleanup/reset deadline | 1 second |+| Logical query message | 64 MiB |+| Connection-wide metadata | 16 MiB |+| Registered prepared statements | 1,024 |+| Bulk rows per command | 10,000 |+| Streamed parameter bytes per execution | 64 MiB |+| Cursor rows per fetch | 65,536 |++See `ConnectionOptions`/`DecodeLimits` Haddock for all fields and server-limit+intersections. The establishment deadline is not an automatic query timeout.+Apply your own deadline when required, accepting terminal retirement if it+interrupts protocol I/O. Never automatically retry uncertain writes.++Start with prepared bindings and ordinary streams. Use bulk for similar writes,+cached metadata for repeated results, reset for request reuse, and optional+features for measured resource/network needs. Read the+[benchmark guide](https://github.com/aantich/mariadb-haskell/blob/main/docs/BENCHMARKS.md).+Report sanitized reproducers with server/compiler versions through+[issues](https://github.com/aantich/mariadb-haskell/issues); never include passwords,+ticket caches, keytabs or private keys.
+ examples/GuideExamples.hs view
@@ -0,0 +1,125 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Complete helpers accompanying docs/USER_GUIDE.md. These are application+-- building blocks, not a schema migration or a runnable benchmark. Compile with:+--+-- @+-- cabal exec -- ghc -fno-code -Wall -package mariadb-haskell examples/GuideExamples.hs+-- @+--+-- Table prerequisites: measurements(sensor_id BIGINT, reading DOUBLE NULL),+-- files(contents LONGBLOB), customers(id BIGINT, name TEXT), and transactional+-- InnoDB tables when transaction guarantees are required. Use your own schema/grants.+module GuideExamples where++import Control.Exception (bracket)+import Control.Monad (unless)+import Data.ByteString (ByteString)+import Data.Int (Int64)+import Data.Word (Word32)+import qualified Database.MySQL.Base as DB+import qualified Database.MySQL.BinLog as CDC+import Database.MySQL.Field (DecodeError, fromField)+import qualified Database.MySQL.TLS as TLS+import qualified Network.TLS as NetworkTLS+import System.IO (IOMode (ReadMode), withBinaryFile)+import qualified System.IO.Streams as Streams++-- | Consume all rows inside the statement bracket; the caller owns conn.+customersFrom :: DB.MySQLConn -> Int64 -> ([DB.MySQLValue] -> IO ()) -> IO ()+customersFrom conn minimumId handleRow =+    DB.withPreparedStatement conn+        "SELECT id, name FROM customers WHERE id >= ? ORDER BY id" $ \statement -> do+            (_, rows) <- DB.queryPrepared statement [DB.MySQLInt64 minimumId]+            Streams.foldM (\() row -> handleRow row) () rows++-- | Decode an already shape-checked BIGINT field without narrowing/coercion.+signedBigInt :: DB.MySQLValue -> Either DecodeError Int64+signedBigInt = fromField++-- | Demonstrate native NULL binding and one concrete type per bulk column.+insertMeasurements :: DB.MySQLConn -> IO DB.OK+insertMeasurements conn =+    DB.withPreparedStatement conn+        "INSERT INTO measurements(sensor_id, reading) VALUES (?, ?)" $ \statement ->+            DB.executeBulk statement+                [ [DB.BulkValue (DB.MySQLInt64 1), DB.BulkValue (DB.MySQLDouble 12.5)]+                , [DB.BulkValue (DB.MySQLInt64 2), DB.BulkValue DB.MySQLNull]+                ]++-- | Wrap a bulk operation in an explicit transaction. The caller must use a+-- transactional table and must not nest this inside another withTransaction.+transactionalMeasurements :: DB.MySQLConn -> IO DB.OK+transactionalMeasurements conn = DB.withTransaction conn (insertMeasurements conn)++-- | The file handle remains open for the entire one-shot upload. Files larger+-- than the configured aggregate upload budget fail rather than silently split.+uploadFile :: DB.MySQLConn -> FilePath -> IO DB.OK+uploadFile conn path = withBinaryFile path ReadMode $ \handle -> do+    source <- Streams.handleToInputStream handle+    DB.withPreparedStatement conn "INSERT INTO files(contents) VALUES (?)" $ \statement ->+        DB.executePreparedStream statement [DB.PreparedBytes source]++-- | Read a cursor in bounded fetches on the creating thread. Ordinary row+-- streaming is often faster; choose cursors for explicit fetch control.+cursorCustomers :: DB.MySQLConn -> Word32 -> ([DB.MySQLValue] -> IO ()) -> IO ()+cursorCustomers conn batchSize handleRow =+    DB.withPreparedStatement conn "SELECT id, name FROM customers ORDER BY id" $ \statement ->+        DB.withCursor statement [] $ \cursor ->+            let loop = do+                    done <- DB.cursorExhausted cursor+                    unless done $ do+                        rows <- DB.fetchCursor cursor batchSize+                        Streams.foldM (\() row -> handleRow row) () rows+                        loop+            in loop++-- | Prepare TLS settings once outside a pool's connection factory.+withVerifiedTLS :: FilePath -> String -> DB.ConnectInfo -> (DB.MySQLConn -> IO a) -> IO a+withVerifiedTLS caPath serverName ci action = do+    params <- TLS.makeClientParams (TLS.CustomCAStore caPath)+    bracket (TLS.connect ci (params, serverName)) DB.close action++-- | A single hidden PAM prompt; use an application callback for multiple factors.+withPAM :: NetworkTLS.ClientParams -> String -> DB.ConnectInfo -> ByteString+        -> (DB.MySQLConn -> IO a) -> IO a+withPAM params serverName ci password action =+    let auth = DB.defaultAuthenticationOptions+            { DB.authenticationDialog = Just (DB.passwordDialog password) }+    in bracket (TLS.connectWithAuthentication DB.defaultConnectionOptions auth ci+                   (params, serverName)) DB.close action++-- | Use an existing ticket cache. Build the executable with -threaded and+-- configure the realm/server separately. Add TLS for SQL confidentiality.+withKerberos :: DB.ConnectInfo -> ByteString -> (DB.MySQLConn -> IO a) -> IO a+withKerberos ci exactPrincipal =+    let auth = DB.defaultAuthenticationOptions+            { DB.authenticationGSSAPIPrincipal = Just exactPrincipal }+    in bracket (DB.connectWithAuthentication DB.defaultConnectionOptions auth ci) DB.close++-- | Configure optional wire features before connection establishment. The+-- callback is synchronous and must never reenter the same connection.+withProgress :: DB.ConnectInfo -> (DB.Progress -> IO ()) -> (DB.MySQLConn -> IO a) -> IO a+withProgress ci callback action =+    let options = DB.defaultConnectionOptions { DB.enableProgressReporting = True }+    in bracket (DB.connectWithOptions options ci) DB.close $ \conn -> do+        DB.setProgressCallback conn (Just callback)+        action conn++-- | Safely consume a dedicated manual-ACK stream. The supplied sink must+-- durably stage each event and, at a checkpoint event, atomically commit the+-- entire group and its exported resume coordinates before returning. It must+-- not retain an unbounded transaction in memory. The driver supplies no sink+-- persistence or exactly-once protocol. Semisync must be configured server-side.+consumeDurably :: DB.MySQLConn -> Word32 -> ByteString -> (CDC.CDCEvent -> IO ()) -> IO ()+consumeDurably conn replicaId savedGtids applyAndPersist = do+    gtids <- either (ioError . userError) pure (CDC.parseGtidSet savedGtids)+    let options = (CDC.defaultCDCOptions (CDC.FromGtid gtids))+            { CDC.cdcAcknowledgements = CDC.AcknowledgeManually }+    bracket (CDC.openCDC conn replicaId options) CDC.cdcClose $ \stream -> do+        let handleEvent event = do+                applyAndPersist event+                case CDC.cdcEventCheckpoint event of+                    Nothing -> pure ()+                    Just checkpoint -> CDC.cdcAcknowledge stream checkpoint+        Streams.foldM (\() event -> handleEvent event) () (CDC.cdcEvents stream)
+ examples/Quickstart.hs view
@@ -0,0 +1,52 @@+{-# LANGUAGE OverloadedStrings #-}++-- | A read-only example with no table/schema prerequisites. From a checkout:+--+-- @+-- cabal build lib:mariadb-haskell+-- cabal exec -- ghc -threaded -package mariadb-haskell examples/Quickstart.hs -o /tmp/mariadb-quickstart+-- @+--+-- Set MDB_USER, MDB_PASSWORD and MDB_DATABASE. Optional MDB_HOST/MDB_PORT+-- default to 127.0.0.1/3306. Set both MDB_TLS_CA and MDB_TLS_NAME for verified+-- TLS; otherwise this example uses TCP, suitable for a trusted local service.+module Main (main) where++import Control.Exception (bracket)+import Data.Maybe (fromMaybe)+import qualified Data.Text as Text+import qualified Data.Text.Encoding as Text+import qualified Database.MySQL.Base as DB+import qualified Database.MySQL.TLS as TLS+import System.Environment (getEnv, lookupEnv)+import qualified System.IO.Streams as Streams+import Text.Read (readMaybe)++main :: IO ()+main = do+    host <- fromMaybe "127.0.0.1" <$> lookupEnv "MDB_HOST"+    portText <- fromMaybe "3306" <$> lookupEnv "MDB_PORT"+    port <- case readMaybe portText :: Maybe Integer of+        Just number | number >= 1 && number <= 65535 -> pure (fromInteger number)+        _ -> ioError (userError "MDB_PORT must be between 1 and 65535")+    user <- utf8 <$> getEnv "MDB_USER"+    password <- utf8 <$> getEnv "MDB_PASSWORD"+    database <- utf8 <$> getEnv "MDB_DATABASE"+    let ci = DB.defaultConnectInfo+            { DB.ciHost = host, DB.ciPort = port, DB.ciUser = user+            , DB.ciPassword = password, DB.ciDatabase = database+            }+    ca <- lookupEnv "MDB_TLS_CA"+    name <- lookupEnv "MDB_TLS_NAME"+    open <- case (ca, name) of+        (Nothing, Nothing) -> pure (DB.connect ci)+        (Just path, Just serverName) -> do+            params <- TLS.makeClientParams (TLS.CustomCAStore path)+            pure (TLS.connect ci (params, serverName))+        _ -> ioError (userError "Set both MDB_TLS_CA and MDB_TLS_NAME for TLS")+    bracket open DB.close $ \conn ->+        DB.withPreparedStatement conn "SELECT VERSION(), ?" $ \statement -> do+            (_, rows) <- DB.queryPrepared statement [DB.MySQLText "Hello, MariaDB 🌍"]+            Streams.foldM (\() row -> print row) () rows+  where+    utf8 = Text.encodeUtf8 . Text.pack
+ mariadb-haskell.cabal view
@@ -0,0 +1,367 @@+cabal-version:      3.4+name:               mariadb-haskell+version:            0.1.0.0+synopsis:           Alpha streaming Haskell driver for MariaDB+stability:          Alpha+description:+  Alpha software: extensively validated with automated tests and benchmarks,+  but not yet validated in real-world applications. Evaluate in your own+  environment before relying on it; APIs and behavior may change.++  MariaDB-only fork of mysql-haskell. Streaming text and binary queries,+  managed prepared statements, native bulk execution, cached/extended metadata,+  session observations and clean connection reset, streamed parameters,+  server cursors, progress reporting and optional transport compression.++  TCP, Unix sockets and verified TLS, including client certificates and+  explicit PAM/Kerberos authentication. Row-based CDC supports MariaDB GTID+  resume, complete-group checkpoints and manual semisync acknowledgements.++  See the packaged README, user guide, migration guide and benchmark guide,+  or <https://github.com/aantich/mariadb-haskell>.+  <https://github.com/aantich/mariadb-haskell/blob/main/docs/USER_GUIDE.md User guide>;+  <https://github.com/aantich/mariadb-haskell/blob/main/docs/BENCHMARKS.md benchmarks>;+  <https://github.com/aantich/mariadb-haskell/blob/main/validation/RESULTS.md live validation>.+  Public modules retain Database.MySQL names; MySQL servers are not supported.+  Full validation currently covers MariaDB 12.3.3 on macOS and Linux.+  Linux builds require zlib and MIT Kerberos development libraries;+  macOS uses the system GSS framework. Windows GSSAPI is unavailable.+license:            BSD-3-Clause AND MPL-2.0+license-files:      LICENSE+                    LICENSE-MPL-2.0+author:             winterland1989+maintainer:         Anton Antich <anton.antich@gmail.com>+copyright:          (c) 2016 Winterland+category:           Database+build-type:         Simple+tested-with:        GHC ==9.4.7, GHC ==9.10.3+extra-doc-files:    README.md+                    docs/CA_BUNDLE.md+                    ChangeLog.md+                    docs/USER_GUIDE.md+                    docs/BENCHMARKS.md+                    docs/MIGRATION.md+                    docs/RELEASING.md+data-files:         mozillaCAStore.pem+extra-source-files:+  examples/*.hs+  tools/update_ca_bundle.py+  tools/check_release.py+  ca-bundle.json+  validation/*.py+  validation/*.md+  validation/*.c+  validation/auth_fixture/Dockerfile+  validation/auth_fixture/.dockerignore+  validation/auth_fixture/*.py+  validation/auth_fixture/*.c+  test/cert/ca-key.pem+  test/cert/ca.pem+  test/cert/server-cert.pem+  test/cert/server-key.pem+  test/cert/server-req.pem+  test/json-data/buffer-builder.json+  test/json-data/dates-fract.json+  test/json-data/dates.json+  test/json-data/example.json+  test/json-data/geometry.json+  test/json-data/integers.json+  test/json-data/jp10.json+  test/json-data/jp100.json+  test/json-data/jp50.json+  test/json-data/numbers.json+  test/json-data/twitter1.json+  test/json-data/twitter10.json+  test/json-data/twitter100.json+  test/json-data/twitter20.json+  test/json-data/twitter50.json++homepage:           https://github.com/aantich/mariadb-haskell+bug-reports:        https://github.com/aantich/mariadb-haskell/issues++source-repository head+  type:     git+  location: https://github.com/aantich/mariadb-haskell++flag validation+  description: Build the developer-only correctness and benchmark harness+  default: False+  manual: True++common common-options+  ghc-options:+    -Wall -Wincomplete-uni-patterns+    -Wincomplete-record-updates -Widentities -Wredundant-constraints+    -Wcpp-undef -fwarn-tabs -Wpartial-fields+    -Wunused-packages+    -fenable-th-splice-warnings+    -fno-omit-yields+  build-depends:+      base >=4.17 && <4.22+  default-language: GHC2021++library+  import: common-options+  exposed-modules:+    Data.Binary.Parser+    Data.Binary.Parser.Numeric+    Data.Binary.Parser.Word8+    Data.Connection+    Data.Int.Int24+    Data.TLSSetting+    Data.Word.Word24+    Database.MySQL.Base+    Database.MySQL.Authentication+    Database.MySQL.BinLog+    Database.MySQL.BinLogProtocol.BinLogEvent+    Database.MySQL.BinLogProtocol.BinLogMeta+    Database.MySQL.BinLogProtocol.BinLogValue+    Database.MySQL.Connection+    Database.MySQL.Field+    Database.MySQL.Protocol.Auth+    Database.MySQL.Protocol.ColumnDef+    Database.MySQL.Protocol.Command+    Database.MySQL.Protocol.Escape+    Database.MySQL.Protocol.MySQLValue+    Database.MySQL.Protocol.Packet+    Database.MySQL.TLS+    System.IO.Streams.TCP+    System.IO.Streams.TLS++  other-modules:      Paths_mariadb_haskell+  autogen-modules:    Paths_mariadb_haskell+  hs-source-dirs:     src+  c-sources:         cbits/gssapi.c+  if os(osx)+    frameworks:      GSS+  else+    if !os(windows)+      extra-libraries: gssapi_krb5+  other-modules:      Database.MySQL.Authentication.GSSAPI+                      Database.MySQL.Query+                      Database.MySQL.Session+                      Database.MySQL.BinLogProtocol.CDC+                      Database.MySQL.Protocol.Progress+                      Database.MySQL.Protocol.Compression+  build-depends:+    asn1-encoding >=0.9 && <0.10,+    asn1-types >=0.3 && <0.4,+    binary >=0.8.3 && <0.9,+    blaze-textual >=0.2 && <0.3,+    bytestring >=0.10.2.0 && <0.12 || ^>=0.12.0,+    bytestring-lexing >=0.5 && <0.6,+    containers >=0.5 && <0.9,+    crypton-x509 >=1.5 && <2,+    crypton-x509-store >=1.5 && <2,+    crypton-x509-system >=1.5 && <2,+    data-default-class >=0.1.2 && <0.2 || ^>=0.2.0,+    deepseq >=1.4.6 && <1.5 || ^>=1.5.0,+    io-streams >=1.2 && <2,+    monad-loops >=0.4 && <0.5,+    network >=2.3 && <4,+    crypton-pem >=0.2.4 && <0.4,+    scientific >=0.3 && <0.4,+    text >=1.1 && <2.1 || ^>=2.1,+    time >=1.5.0 && <1.12 || ^>=1.12.2 || ^>=1.14,+    tls >=1.7.0 && <1.8 || ^>=1.8.0 || ^>=1.9.0 || ^>=2.0.0 || ^>=2.1.0 || ^>=2.2.0 || ^>=2.3.0 || ^>=2.4.0,+    vector >=0.8 && <0.13 || ^>=0.13.0,+    word-compat >=0.0 && <0.1,+    zlib >=0.6 && <0.8++  -- Decision: require crypton 2.0, the first release with its side-channel+  -- fixes (none backported to 1.x). Before it, P-384/P-521 point multiplication+  -- and DH exponentiation took time that followed the secret, and tls runs them+  -- on the client's ephemeral key whenever the server picks such a group. 2.0+  -- rather than 2.1.x, as 2.0 has every fix we reach.+  build-depends:+    crypton ^>=2.0.0 || ^>=2.1.0,+    ram >=0.20 && <0.23++  default-extensions:+    DeriveDataTypeable+    DeriveGeneric+    MultiWayIf+    OverloadedStrings+++test-suite test+  import: common-options+  type:               exitcode-stdio-1.0+  main-is:            Main.hs+  other-modules:+    Aeson+    AesonBP+    BoundsCheck+    ConnectInfo+    FieldRoundtrip+    JSON+    QC.ByteString+    QC.Combinator+    QC.Common+    Orphans+    Sha1Scramble+    Sha256Scramble+    TCPStreams+    Word24++  hs-source-dirs:     test+  build-depends:+    attoparsec < 1.0,+    base16-bytestring >=1.0 && <2.0,+    binary >=0.8,+    bytestring >=0.10,+    deepseq,+    directory < 2.0,+    filepath < 2.0,+    io-streams,+    mariadb-haskell,+    network,+    QuickCheck >=2.7 && <3.0,+    quickcheck-instances < 1.0,+    scientific >=0.3.0,+    tasty >=0.11 && <2.0,+    tasty-expected-failure < 1.0,+    tasty-hunit < 1.0,+    tasty-quickcheck >=0.8 && < 1.0,+    text,+    time,+    unordered-containers < 1.0,+    vector++  default-extensions:+    MultiWayIf+    OverloadedStrings++  ghc-options:        -threaded ++test-suite integration+  type:               exitcode-stdio-1.0+  main-is:            Integration.hs+  other-modules:+    BinaryRow+    BinaryRowNew+    BinLog+    BinLogNew+    CachingSha2+    ExecuteMany+    MultipleResults+    MysqlTests+    QueryWithoutResultSet+    RoundtripBit+    RoundtripYear+    SelectOne+    TextRow+    TLSConnection+    TextRowNew+    UnixSocket++  hs-source-dirs:     test+  build-depends:+    base,+    bytestring >=0.10,+    directory,+    io-streams,+    mariadb-haskell,+    tasty >=0.11 && <2.0,+    tasty-hunit < 1.0,+    text,+    time,+    vector++  default-extensions:+    MultiWayIf+    OverloadedStrings++  ghc-options:        -threaded+  default-language:   Haskell2010++benchmark binary-parsers-bench+  import: common-options+  other-modules:+    Aeson+    AesonBP+    Common+    HttpReq+    Network.Wai.Handler.Warp.ReadInt+    Network.Wai.Handler.Warp.RequestHeader++  build-depends:+    attoparsec,+    binary,+    bytestring,+    case-insensitive < 2,+    criterion >=1.1 && <1.2 || ^>=1.6.3,+    deepseq,+    directory < 1.4,+    filepath < 1.6,+    http-types < 1.0,+    mariadb-haskell,+    scanner < 0.4,+    scientific,+    text,+    unordered-containers,+    vector++  hs-source-dirs:   binary-parser-bench+  main-is:          Bench.hs+  type:             exitcode-stdio-1.0++benchmark bench24+  import: common-options+  type:             exitcode-stdio-1.0+  hs-source-dirs:   word24-bench+  main-is:          Benchmark.hs+  build-depends:+    criterion >=1.1,+    deepseq >=1.2 && <2,+    mariadb-haskell+++-- enable to debug the RULES    +-- executable exe+--   import: common-options+--   main-is: Main.hs+--   hs-source-dirs:+--       app+--   build-depends:+--       mariadb-haskell+--   ghc-options: -Wno-unused-packages++-- Developer harness, requiring a provisioned server and a Git/evidence checkout+-- for the complete Python workflow. See docs/RELEASING.md before distribution.+executable mariadb-validation+  import: common-options+  if !flag(validation)+    buildable: False+  default-extensions: OverloadedStrings+  hs-source-dirs: validation/hs+  main-is: Main.hs+  other-modules:+    Validation.Common+    Validation.Protocol+    Validation.Functional+    Validation.Reliability+    Validation.Security+    Validation.CDC+    Validation.Benchmark+    Validation.Supplementary+    Validation.Stage1+    Validation.Stage2+    Validation.Stage3+    Validation.Stage3Bench+    Validation.Stage4+    Validation.Stage4Bench+    Validation.Stage5+    Validation.Stage5Review+    Validation.Stage5Bench+    Validation.Stage6+    Validation.Stage6Bench+    Validation.Stage6Review+    Validation.Stage7+    Validation.Stage7Bench+    Validation.ExternalAuthFixture+    Validation.WireProxy+    Validation.FaultTransport+  build-depends:+    binary, bytestring, containers, filepath >=1.4 && <1.6, io-streams, mariadb-haskell, network, scientific, text, time, tls, vector, zlib, process >=1.6 && <1.7, directory >=1.3 && <1.4+  ghc-options: -threaded -rtsopts -with-rtsopts=-T
+ mozillaCAStore.pem view
@@ -0,0 +1,3016 @@+##+## Bundle of CA Root Certificates+##+## Certificate data from Mozilla as of: Thu Nov  5 04:12:01 2026 GMT+##+## Find updated versions here: https://curl.se/docs/caextract.html+##+## This is a bundle of X.509 certificates of public Certificate Authorities+## (CA). These were automatically extracted from Mozilla's root certificates+## file (certdata.txt).  This file can be found in the mozilla source tree:+## https://raw.githubusercontent.com/mozilla-firefox/firefox/refs/heads/release/security/nss/lib/ckfw/builtins/certdata.txt+##+## It contains the certificates in PEM format and therefore+## can be directly used with curl / libcurl / php_curl, or with+## an Apache+mod_ssl webserver for SSL client authentication.+## Configure this file as the SSLCACertificateFile.+##+## Conversion done with mk-ca-bundle.pl version 1.35.+## SHA256: 3ec2b4d240dc12c747eaf450061db84af757c483bbda302f9e29ceb5d70be510+##+++COMODO ECC Certification Authority+==================================+# Fingerprint (SHA-256): 17:93:92:7A:06:14:54:97:89:AD:CE:2F:8F:34:F7:F0:B6:6D:0F:3A:E3:A3:B8:4D:21:EC:15:DB:BA:4F:AD:C7+-----BEGIN CERTIFICATE-----+MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTELMAkGA1UEBhMC+R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE+ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBB+dXRob3JpdHkwHhcNMDgwMzA2MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0Ix+GzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR+Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRo+b3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSRFtSrYpn1PlILBs5BAH+X+4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0JcfRK9ChQtP6IHG4/bC8vCVlbpVsLM5ni+wz2J+Wos77LTBumjQjBAMB0GA1UdDgQWBBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8E+BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VG+FAkK+qDmfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdvGDeA+U/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY=+-----END CERTIFICATE-----++NetLock Arany (Class Gold) Főtanúsítvány+========================================+# Fingerprint (SHA-256): 6C:61:DA:C3:A2:DE:F0:31:50:6B:E0:36:D2:A6:FE:40:19:94:FB:D1:3D:F9:C8:D4:66:59:92:74:C4:46:EC:98+-----BEGIN CERTIFICATE-----+MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQGEwJIVTERMA8G+A1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3MDUGA1UECwwuVGFuw7pzw610+dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBB+cmFueSAoQ2xhc3MgR29sZCkgRsWRdGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgx+MjA2MTUwODIxWjCBpzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxO+ZXRMb2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlmaWNhdGlv+biBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNzIEdvbGQpIEbFkXRhbsO6+c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxCRec75LbRTDofTjl5Bu+0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrTlF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw+/HpYzY6b7cNGbIRwXdrzAZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAk+H3B5r9s5VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRGILdw+fzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2BJtr+UBdADTHLpl1+neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIB+BjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2MU9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwW+qZw8UQCgwBEIBaeZ5m8BiFRhbvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTta+YtOUZcTh5m2C+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC+bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2FuLjbvrW5Kfna+NwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2XjG4Kvte9nHfRCaexOYNkbQu+dZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E=+-----END CERTIFICATE-----++Microsec e-Szigno Root CA 2009+==============================+# Fingerprint (SHA-256): 3C:5F:81:FE:A5:FA:B8:2C:64:BF:A2:EA:EC:AF:CD:E8:E0:77:FC:86:20:A7:CA:E5:37:16:3D:F3:6E:DB:F3:78+-----BEGIN CERTIFICATE-----+MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYDVQQGEwJIVTER+MA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jv+c2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o+dTAeFw0wOTA2MTYxMTMwMThaFw0yOTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UE+BwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUt+U3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTCCASIw+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvPkd6mJviZpWNwrZuuyjNA+fW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tccbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG+0IMZfcChEhyVbUr02MelTTMuhTlAdX4UfIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKA+pxn1ntxVUwOXewdI/5n7N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm+1HxdrtbCxkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1+rUC+AwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTLD8bf+QkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAbBgNVHREE+FDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqGSIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0o+lZMEyL/azXm4Q5DwpL7v8u8hmLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfX+I/OMn74dseGkddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775+tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c2Pm2G2JwCz02+yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5tHMN1Rq41Bab2XD0h7lbwyYIi+LXpUq3DDfSJlgnCW+-----END CERTIFICATE-----++GlobalSign Root CA - R3+=======================+# Fingerprint (SHA-256): CB:B5:22:D7:B7:F1:27:AD:6A:01:13:86:5B:DF:1C:D4:10:2E:7D:07:59:AF:63:5A:7C:F4:72:0D:C9:63:C5:3B+-----BEGIN CERTIFICATE-----+MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4GA1UECxMXR2xv+YmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh+bFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT+aWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln+bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWt+iHL8RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsTgHeMCOFJ+0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmmKPZpO/bLyCiR5Z2KYVc3+rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zdQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjl+OCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2+xmmFghcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE+FI/wS3+oLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZURUm7+lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMpjjM5RcOO5LlXbKr8+EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK6fBdRoyV3XpYKBovHd7NADdBj+1E+bddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQXmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18+YIvDQVETI53O9zJrlAGomecsMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7r+kpeDMdmztcpHWD9f+-----END CERTIFICATE-----++Izenpe.com+==========+# Fingerprint (SHA-256): 25:30:CC:8E:98:32:15:02:BA:D9:6F:9B:1F:BA:1B:09:9E:2D:29:9E:0F:45:48:BB:91:4F:36:3B:C0:D4:53:1F+-----BEGIN CERTIFICATE-----+MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4MQswCQYDVQQG+EwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wHhcNMDcxMjEz+MTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMu+QS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ+03rKDx6sp4boFmVqscIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAK+ClaOxdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6HLmYRY2xU++zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFXuaOKmMPsOzTFlUFpfnXC+PCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQDyCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxT+OTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbK+F7jJeodWLBoBHmy+E60QrLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK+0GqfvEyNBjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8Lhij++0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIBQFqNeb+Lz0vPqhbB+leStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+HMh3/1uaD7euBUbl8agW7EekFwID+AQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2luZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw++SVpFTlBFIFMuQS4gLSBDSUYgQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBG+NjIgUzgxQzBBBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx+MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O+BBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUAA4ICAQB4pgwWSp9MiDrAyw6l+Fn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWblaQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbga+kEyrkgPH7UIBzg/YsfqikuFgba56awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8q+hT/AQKM6WfxZSzwoJNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Cs+g1lwLDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCTVyvehQP5+aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGkLhObNA5me0mrZJfQRsN5+nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJbUjWumDqtujWTI6cfSN01RpiyEGjkpTHC+ClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZo+Q0iy2+tzJOeRf1SktoA+naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1Z+WrOZyGlsQyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw==+-----END CERTIFICATE-----++Go Daddy Root Certificate Authority - G2+========================================+# Fingerprint (SHA-256): 45:14:0B:32:47:EB:9C:C8:C5:B4:F0:D7:B5:30:91:F7:32:92:08:9E:6E:5A:63:E2:74:9D:D3:AC:A9:19:8E:DA+-----BEGIN CERTIFICATE-----+MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT+B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHkuY29tLCBJbmMu+MTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5+MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6+b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8G+A1UEAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI+hvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKDE6bFIEMBO4Tx5oVJnyfq+9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD++qK+ihVqf94Lw7YZFAXK6sOoBJQ7RnwyDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutd+fMh8+7ArU6SSYmlRJQVhGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMl+NAJWJwGRtDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEAAaNC+MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqahQcQZyi27/a9+BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmXWWcDYfF+OwYxdS2hII5PZYe096ac+vNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r+5N9ss4UXnT3ZJE95kTXWXwTrgIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYV+N8Gb5DKj7Tjo2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO+LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI4uJEvlz36hz1+-----END CERTIFICATE-----++Starfield Root Certificate Authority - G2+=========================================+# Fingerprint (SHA-256): 2C:E1:CB:0B:F9:D2:F9:E1:02:99:3F:BE:21:51:52:C3:B2:DD:0C:AB:DE:1C:68:E5:31:9B:83:91:54:DB:B7:F5+-----BEGIN CERTIFICATE-----+MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT+B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s+b2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVsZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0+eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAw+DgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQg+VGVjaG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZpY2F0ZSBB+dXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3twQP89o/8ArFv+W59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMgnLRJdzIpVv257IzdIvpy3Cdhl+72WoTs+bhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNk+N3mSwOxGXn/hbVNMYq/NHwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7Nf+ZTD4p7dNdloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0HZbU+JtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC+AQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0GCSqGSIb3DQEBCwUAA4IBAQARWfol+TwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjUsHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx+4mcujJUDJi5DnUox9g61DLu34jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUw+F5okxBDgBPfg8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K+pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1mMpYjn0q7pBZ+c2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0+-----END CERTIFICATE-----++Starfield Services Root Certificate Authority - G2+==================================================+# Fingerprint (SHA-256): 56:8D:69:05:A2:C8:87:08:A4:B3:02:51:90:ED:CF:ED:B1:97:4A:60:6A:13:C6:E5:29:0F:CB:2A:E6:3E:DA:B5+-----BEGIN CERTIFICATE-----+MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMxEDAOBgNVBAgT+B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s+b2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRl+IEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNV+BAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxT+dGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2VydmljZXMg+Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC+AQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20pOsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2+h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm28xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4Pa+hHQUw2eeBGg6345AWh1KTs9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLP+LJGmpufehRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk6mFB+rMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAwDwYDVR0TAQH/BAUw+AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMA0GCSqG+SIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMIbw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPP+E95Dz+I0swSdHynVv/heyNXBve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTy+xQGjhdByPq1zqwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd+iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn0q23KXB56jza+YyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCNsSi6+-----END CERTIFICATE-----++Certum Trusted Network CA+=========================+# Fingerprint (SHA-256): 5C:58:46:8D:55:F5:8E:49:7E:74:39:82:D2:B5:00:10:B6:D1:65:37:4A:CF:83:A7:D4:A3:2D:B7:68:C4:40:8E+-----BEGIN CERTIFICATE-----+MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK+ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlv+biBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIy+MTIwNzM3WhcNMjkxMjMxMTIwNzM3WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBU+ZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5+MSIwIAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC+AQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rHUV+rpDKmYYe2bg+G0jAC+l/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LMTXPb865Px1bVWqeWifrzq2jUI4ZZJ88J+J7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVUBBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4+fOQtf/WsX+sWn7Et0brMkUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0+cvW0QM8xAcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNVHRMB+Af8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNVHQ8BAf8EBAMCAQYw+DQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15ysHhE49wcrwn9I0j6vSrEuVUEtRCj+jSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfLI9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1+mS1FhIrlQgnXdAIv94nYmem8J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5aj+Zt3hrvJBW8qYVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI+03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw=+-----END CERTIFICATE-----++TWCA Root Certification Authority+=================================+# Fingerprint (SHA-256): BF:D8:8F:E1:10:1C:41:AE:3E:80:1B:F8:BE:56:35:0E:E9:BA:D1:A6:B9:BD:51:5E:DC:5C:6D:5B:87:11:AC:44+-----BEGIN CERTIFICATE-----+MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJ+VEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlmaWNh+dGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMzWhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQG+EwJUVzESMBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NB+IFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK+AoIBAQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFEAcK0HMMx+QhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HHK3XLfJ+utdGdIzdjp9xC+oi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeXRfwZVzsrb+RH9JlF/h3x+JejiB03HFyP+4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/zrX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1r+y+UPizgN7gr8/g+YnzAx3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIB+BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkqhkiG+9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeCMErJk/9q56YAf4lC+mtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdlsXebQ79NqZp4VKIV66IIArB6nCWlW+QtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62Dlhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVY+T0bf+215WfKEIlKuD8z7fDvnaspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocny+Yh0igzyXxfkZYiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw==+-----END CERTIFICATE-----++Security Communication RootCA2+==============================+# Fingerprint (SHA-256): 51:3B:2C:EC:B8:10:D4:CD:E5:DD:85:39:1A:DF:C6:C2:DD:60:D8:7B:B7:36:D2:B5:21:48:4A:A4:7A:0E:BE:F6+-----BEGIN CERTIFICATE-----+MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDElMCMGA1UEChMc+U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMeU2VjdXJpdHkgQ29tbXVuaWNh+dGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoXDTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMC+SlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3Vy+aXR5IENvbW11bmljYXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB+ANAVOVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGrzbl+dp++++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVMVAX3NuRFg3sUZdbcDE3R+3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQhNBqyjoGADdH5H5XTz+L62e4iKrFvlNV+spHEfbmwhRkGeC7bYRr6hfVKkaHnFtWOojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1K+EOtOghY6rCcMU/Gt1SSwawNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8+QIH4D5csOPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB+CwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpFcoJxDjrSzG+ntKEj+u/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXcokgfGT+Ok+vx+hfuzU7jBBJV1uXk+3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6q+tnRGEmyR7jTV7JqR50S+kDFy1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29+mvVXIwAHIRc/SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03+-----END CERTIFICATE-----++Actalis Authentication Root CA+==============================+# Fingerprint (SHA-256): 55:92:60:84:EC:96:3A:64:B9:6E:2A:BE:01:CE:0B:A8:6A:64:FB:FE:BC:C7:AA:B5:AF:C1:55:B3:7F:D7:60:66+-----BEGIN CERTIFICATE-----+MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCSVQxDjAM+BgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UE+AwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDky+MjExMjIwMlowazELMAkGA1UEBhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlz+IFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290+IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNvUTufClrJ+wkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX4ay8IMKx4INRimlNAJZa+by/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9KK3giq0itFZljoZUj5NDKd45RnijMCO6+zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1f+YVEiVRvjRuPjPdA1YprbrxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2+oxgkg4YQ51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2Fbe8l+EfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxeKF+w6D9Fz8+vm2/7+hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4Fv6MGn8i1zeQf1xcGDXqVdFUNaBr8+EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbnfpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5+jF66CyCU3nuDuP/jVo23Eek7jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLY+iDrIn3hm7YnzezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt+ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQALe3KHwGCmSUyI+WOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70jsNjLiNmsGe+b7bAEzlgqqI0+JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDzWochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKx+K3JCaKygvU5a2hi/a5iB0P2avl4VSM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU++Xlff1ANATIGk0k9jpwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC+4yyXX04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+OkfcvHlXHo+2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7RK4X9p2jIugErsWx0Hbhz+lefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btUZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXem+OR/qnuOf0GZvBeyqdn6/axag67XH/JJULysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9+vwGYT7JZVEc+NHt4bVaTLnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg==+-----END CERTIFICATE-----++Buypass Class 2 Root CA+=======================+# Fingerprint (SHA-256): 9A:11:40:25:19:7C:5B:B9:5D:94:E6:3D:55:CD:43:79:08:47:B6:46:B2:3C:DF:11:AD:A4:A0:0E:FF:15:FB:48+-----BEGIN CERTIFICATE-----+MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU+QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMiBSb290IENBMB4X+DTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1owTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1+eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIw+DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1+g1Lr6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPVL4O2fuPn+9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC911K2GScuVr1QGbNgGE41b+/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHxMlAQTn/0hpPshNOOvEu/XAFOBz3cFIqU+CqTqc/sLUegTBxj6DvEr0VQVfTzh97QZQmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeff+awrbD02TTqigzXsu8lkBarcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgI+zRFo1clrUs3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLiFRhn+Bkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRSP/TizPJhk9H9Z2vX+Uq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN9SG9dKpN6nIDSdvHXx1iY8f93ZHs+M+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxPAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD+VR0OBBYEFMmAd+BikoL1RpzzuvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF+AAOCAgEAU18h9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s+A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3tOluwlN5E40EI+osHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo+fsicdl9sz1Gv7SEr5AcD48S+aq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYd+DnkM/crqJIByw5c/8nerQyIKx+u2DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWD+LfJ6v9r9jv6ly0UsH8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0+oyLQI+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK75t98biGC+wWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h3PFaTWwyI0PurKju7koS+CTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPzY11aWOIv4x3kqdbQCtCev9eBCfHJxyYN+rJgWVqA=+-----END CERTIFICATE-----++Buypass Class 3 Root CA+=======================+# Fingerprint (SHA-256): ED:F7:EB:BC:A2:7A:2A:38:4D:38:7B:7D:40:10:C6:66:E2:ED:B4:84:3E:4C:29:B4:AE:1D:5B:93:32:E6:B2:4D+-----BEGIN CERTIFICATE-----+MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU+QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMyBSb290IENBMB4X+DTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFowTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1+eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIw+DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRH+sJ8YZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3EN3coTRiR+5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9tznDDgFHmV0ST9tD+leh+7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX0DJq1l1sDPGzbjniazEuOQAnFN44wOwZ+ZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH+2xc519woe2v1n/MuwU8XKhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV+/afmiSTYzIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvSO1UQ+RwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D34xFMFbG02SrZvPA+Xpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgPK9Dx2hzLabjKSWJtyNBjYt1gD1iq+j6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD+VR0OBBYEFEe4zf/lb+74suwvTg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF+AAOCAgEAACAjQTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV+cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXSIGrs/CIBKM+G+uIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2HJLw5QY33KbmkJs4j1xrG0aG+Q0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsaO5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8+ZORK15FTAaggiG6cX0S5y2CBNOxv033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2+KSb12tjE8nVhz36udmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz+6MkEkbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg413OEMXbug+UZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvDu79leNKGef9JOxqDDPDe+eOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq4/g7u9xN12TyUb7mqqta6THuBrxzvxNi+Cp/HuZc=+-----END CERTIFICATE-----++T-TeleSec GlobalRoot Class 3+============================+# Fingerprint (SHA-256): FD:73:DA:D3:1C:64:4F:F1:B4:3B:EF:0C:CD:DA:96:71:0B:9C:D9:87:5E:CA:7E:31:70:7A:F3:E9:6D:52:2B:BD+-----BEGIN CERTIFICATE-----+MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM+IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU+cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgx+MDAxMTAyOTU2WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz+dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD+ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0GCSqGSIb3+DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN8ELg63iIVl6bmlQdTQyK+9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/RLyTPWGrTs0NvvAgJ1gORH8EGoel15YU+NpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZF+iP0Zf3WHHx+xGwpzJFu5ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W+0eDrXltMEnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGjQjBA+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1A/d2O2GCahKqGFPr+AyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOyWL6ukK2YJ5f+AbGwUgC4TeQbIXQb+fsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzT+ucpH9sry9uetuUg/vBa3wW306gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7h+P0HHRwA11fXT91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml+e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4pTpPDpFQUWw==+-----END CERTIFICATE-----++D-TRUST Root Class 3 CA 2 2009+==============================+# Fingerprint (SHA-256): 49:E7:A4:42:AC:F0:EA:62:87:05:00:54:B5:25:64:B6:50:E4:F4:9E:42:E3:48:D6:AA:38:E0:39:E9:57:B1:C1+-----BEGIN CERTIFICATE-----+MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQK+DAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTAe+Fw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NThaME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxE+LVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIw+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOAD+ER03UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42tSHKXzlA+BF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9RySPocq60vFYJfxLLHLGv+KZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsMlFqVlNpQmvH/pStmMaTJOKDfHR+4CS7z+p+hnUquVH+BGPtikw8paxTGA6Eian5Rp/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUC+AwEAAaOCARowggEWMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ+4PGEMA4GA1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVjdG9y+eS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUyMENBJTIwMiUyMDIw+MDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRlcmV2b2NhdGlvbmxpc3QwQ6BBoD+G+PWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAw+OS5jcmwwDQYJKoZIhvcNAQELBQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm+2H6NMLVwMeniacfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0+o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4KzCUqNQT4YJEV+dT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8PIWmawomDeCTmGCufsYkl4ph+X5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3YJohw1+qRzT65ysCQblrGXnRl11z+o+I=+-----END CERTIFICATE-----++D-TRUST Root Class 3 CA 2 EV 2009+=================================+# Fingerprint (SHA-256): EE:C5:49:6B:98:8C:E9:86:25:B9:34:09:2E:EC:29:08:BE:D0:B0:F3:16:C2:D4:73:0C:84:EA:F1:F3:D3:48:81+-----BEGIN CERTIFICATE-----+MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK+DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw+OTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUwNDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK+DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw+OTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfS+egpnljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM03TP1YtHh+zRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6ZqQTMFexgaDbtCHu39b+T+7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lRp75mpoo6Kr3HGrHhFPC+Oh25z1uxav60+sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure35+11H3a6UCAwEAAaOCASQwggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyv+cop9NteaHNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFwOi8v+ZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xhc3MlMjAzJTIwQ0El+MjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1ERT9jZXJ0aWZpY2F0ZXJldm9jYXRp+b25saXN0MEagRKBChkBodHRwOi8vd3d3LmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xh+c3NfM19jYV8yX2V2XzIwMDkuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P++PPoeUSbrh/Yp3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05+nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNFCSuGdXzfX2lX+ANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7naxpeG0ILD5EJt/rDiZE4OJudA+NCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqXKVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVv+w9y4AyHqnxbxLFS1+-----END CERTIFICATE-----++CA Disig Root R2+================+# Fingerprint (SHA-256): E2:3D:4A:03:6D:7B:70:E9:F5:95:B1:42:20:79:D2:B9:1E:DF:BB:1F:B6:51:A0:63:3E:AA:8A:9D:C5:F8:07:03+-----BEGIN CERTIFICATE-----+MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNVBAYTAlNLMRMw+EQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMuMRkwFwYDVQQDExBDQSBEaXNp+ZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQyMDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sx+EzARBgNVBAcTCkJyYXRpc2xhdmExEzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERp+c2lnIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbC+w3OeNcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNHPWSb6Wia+xswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3Ix2ymrdMxp7zo5eFm1tL7+A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbeQTg06ov80egEFGEtQX6sx3dOy1FU+16S+GBsEWmjGycT6txOgmLcRK7fWV8x8nhfRyyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqV+g8NTEQxzHQuyRpDRQjrOQG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa+5Beny912H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJQfYE+koopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUDi/ZnWejBBhG93c+A+Ak9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORsnLMOPReisjQS1n6yqEm70XooQL6i+Fh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNV+HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5u+Qu0wDQYJKoZIhvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM+tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqfGopTpti72TVV+sRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkblvdhuDvEK7Z4bLQjb/D907Je+dR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka+elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W8+1k/BfDxujRNt+3vrMNDcTa/F1balTFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjx+mHHEt38OFdAlab0inSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01+utI3gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18DrG5gPcFw0+sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3OszMOl6W8KjptlwlCFtaOg+UxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8xL4ysEr3vQCj8KWefshNPZiTEUxnpHikV+7+ZtsH8tZ/3zbBt1RqPlShfppNcL+-----END CERTIFICATE-----++ACCVRAIZ1+=========+# Fingerprint (SHA-256): 9A:6E:C0:12:E1:A7:DA:9D:BE:34:19:4D:47:8A:D7:C0:DB:18:22:FB:07:1D:F1:29:81:49:6E:D1:04:38:41:13+-----BEGIN CERTIFICATE-----+MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UEAwwJQUNDVlJB+SVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQswCQYDVQQGEwJFUzAeFw0xMTA1+MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQBgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwH+UEtJQUNDVjENMAsGA1UECgwEQUNDVjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4IC+DwAwggIKAoICAQCbqau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gM+jmoYHtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWoG2ioPej0+RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpAlHPrzg5XPAOBOp0KoVdD+aaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhrIA8wKFSVf+DuzgpmndFALW4ir50awQUZ+0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDG+WuzndN9wrqODJerWx5eHk6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs7+8yM2x/474KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMOm3WR+5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpacXpkatcnYGMN285J+9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPluUsXQA+xtrn13k/c4LOsOxFwYIRK+Q26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYIKwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRw+Oi8vd3d3LmFjY3YuZXMvZmlsZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEu+Y3J0MB8GCCsGAQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2+VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeTVfZW6oHlNsyM+Hj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIGCCsGAQUFBwICMIIBFB6CARAA+QQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUAcgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBh+AO0AegAgAGQAZQAgAGwAYQAgAEEAQwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUA+YwBuAG8AbABvAGcA7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBj+AHQAcgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAAQwBQAFMA+IABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUAczAwBggrBgEFBQcCARYk+aHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2MuaHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0+dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRtaW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2+MV9kZXIuY3JsMA4GA1UdDwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZI+hvcNAQEFBQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdpD70E+R9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gUJyCpZET/LtZ1qmxN+YEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+mAM/EKXMRNt6GGT6d7hmKG9Ww7Y49+nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepDvV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJ+TS+xJlsndQAJxGJ3KQhfnlmstn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3+sCPdK6jT2iWH7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h+I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szAh1xA2syVP1Xg+Nce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xFd3+YJ5oyXSrjhO7FmGYvliAd+3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2HpPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3p+EfbRD0tVNEYqi4Y7+-----END CERTIFICATE-----++TWCA Global Root CA+===================+# Fingerprint (SHA-256): 59:76:90:07:F7:68:5D:0F:CD:50:87:2F:9F:95:D5:75:5A:5B:2B:45:7D:81:F3:69:2B:61:0A:98:67:2F:0E:1B+-----BEGIN CERTIFICATE-----+MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcxEjAQBgNVBAoT+CVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMTVFdDQSBHbG9iYWwgUm9vdCBD+QTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQK+EwlUQUlXQU4tQ0ExEDAOBgNVBAsTB1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3Qg+Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2C+nJfF10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz0ALfUPZV+r2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfChMBwqoJimFb3u/Rk28OKR+Q4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbHzIh1HrtsBv+baz4X7GGqcXzGHaL3SekV+tTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1W+KKD+u4ZqyPpcC1jcxkt2yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99+sy2sbZCilaLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYPoA/p+yJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQABDzfuBSO6N+pjWxn+kjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcEqYSjMq+u7msXi7Kx/mzhkIyIqJdI+zshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMC+AQYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6g+cFGn90xHNcgL1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn+LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WFH6vPNOw/KP4M+8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNoRI2T9GRwoD2dKAXDOXC4Ynsg+/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlg+lPx4mI88k1HtQJAH32RjJMtOcQWh15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryP+A9gK8kxkRr05YuWW6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3m+i4TWnsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5jwa19hAM8+EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWzaGHQRiapIVJpLesux+t3+zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmyKwbQBM0=+-----END CERTIFICATE-----++T-TeleSec GlobalRoot Class 2+============================+# Fingerprint (SHA-256): 91:E2:F5:78:8D:58:10:EB:A7:BA:58:73:7D:E1:54:8A:8E:CA:CD:01:45:98:BC:0B:14:3E:04:1B:17:05:25:52+-----BEGIN CERTIFICATE-----+MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM+IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU+cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgx+MDAxMTA0MDE0WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz+dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD+ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0GCSqGSIb3+DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUdAqSzm1nzHoqvNK38DcLZ+SBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiCFoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/F+vudocP05l03Sx5iRUKrERLMjfTlH6VJi1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx970+2cu+fjOlbpSD8DT6IavqjnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGV+WOHAD3bZwI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGjQjBA+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/WSA2AHmgoCJrjNXy+YdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhyNsZt+U2e+iKo4YFWz827n+qrkRk4+r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPACuvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNf+vNoBYimipidx5joifsFvHZVwIEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR+3p1m0IvVVGb6g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN+9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlPBSeOE6Fuwg==+-----END CERTIFICATE-----++QuoVadis Root CA 1 G3+=====================+# Fingerprint (SHA-256): 8A:86:6F:D1:B2:76:B5:7E:57:8E:92:1C:65:82:8A:2B:ED:58:E9:F2:F2:88:05:41:34:B7:F1:F4:BF:C9:CC:74+-----BEGIN CERTIFICATE-----+MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQELBQAwSDELMAkG+A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv+b3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJN+MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEg+RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakE+PBtVwedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWerNrwU8lm+PNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF34168Xfuw6cwI2H44g4hWf6+Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh4Pw5qlPafX7PGglTvF0FBM+hSo+LdoIN+ofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXpUhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/l+g6AnhF4EwfWQvTA9xO+oabw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV+7qJZjqlc3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/GKubX+9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSthfbZxbGL0eUQMk1f+iyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KOTk0k+17kBL5yG6YnLUlamXrXXAkg+t3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOtzCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD+AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZI+hvcNAQELBQADggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC+MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2cDMT/uFPpiN3+GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUNqXsCHKnQO18LwIE6PWThv6ct+Tr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP++V04ikkwj+3x6xn0dxoxGE1nVGwvb2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh+3jRJjehZrJ3ydlo28hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fa+wx/kNSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNjZgKAvQU6+O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhpq1467HxpvMc7hU6eFbm0+FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFtnh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOV+hMJKzRwuJIczYOXD+-----END CERTIFICATE-----++QuoVadis Root CA 2 G3+=====================+# Fingerprint (SHA-256): 8F:E4:FB:0A:F9:3A:4D:0D:67:DB:0B:EB:B2:3E:37:C7:1B:F3:25:DC:BC:DD:24:0E:A0:4D:AF:58:B4:7E:18:40+-----BEGIN CERTIFICATE-----+MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQELBQAwSDELMAkG+A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv+b3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJN+MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIg+RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFh+ZiFfqq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMWn4rjyduY+NM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ymc5GQYaYDFCDy54ejiK2t+oIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+o+MiwMzAkd056OXbxMmO7FGmh77FOm6RQ1o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+l+V0POKa2Mq1W/xPtbAd0jIaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZo+L1NesNKqIcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz8eQQ+sSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43ehvNURG3YBZwjgQQvD+6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l7ZizlWNof/k19N+IxWA1ksB8aRxh+lRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALGcC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTAD+AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZI+hvcNAQELBQADggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66+AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RCroijQ1h5fq7K+pVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0GaW/ZZGYjeVYg3UQt4XAoeo0L9+x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4nlv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgz+dWqTHBLmYF5vHX/JHyPLhGGfHoJE+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6X+U/IyAgkwo1jwDQHVcsaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+Nw+mNtddbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNgKCLjsZWD+zYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeMHVOyToV7BjjHLPj4sHKN+JeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4WSr2Rz0ZiC3oheGe7IUIarFsNMkd7Egr+O3jtZsSOeWmD3n+M+-----END CERTIFICATE-----++QuoVadis Root CA 3 G3+=====================+# Fingerprint (SHA-256): 88:EF:81:DE:20:2E:B0:18:45:2E:43:F8:64:72:5C:EA:5F:BD:1F:C2:D9:D2:05:73:07:09:C5:D8:B8:69:0F:46+-----BEGIN CERTIFICATE-----+MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQELBQAwSDELMAkG+A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv+b3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJN+MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMg+RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286+IxSR/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNuFoM7pmRL+Mon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXRU7Ox7sWTaYI+FrUoRqHe+6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+cra1AdHkrAj80//ogaX3T7mH1urPnMNA3+I4ZyYUUpSFlob3emLoG+B01vr87ERRORFHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3U+VDmrJqMz6nWB2i3ND0/kA9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f7+5li59wzweyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634RylsSqi+Md5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBpVzgeAVuNVejH38DM+dyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0QA4XN8f+MFrXBsj6IbGB/kE+V9/Yt+rQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD+AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZI+hvcNAQELBQADggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px+KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnIFUBhynLWcKzS+t/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5WvvoxXqA/4Ti2Tk08HS6IT7SdEQ+TXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFgu/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9Du+DcpmvJRPpq3t/O5jrFc/ZSXPsoaP0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGib+Ih6BJpsQBJFxwAYf3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmD+hPbl8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+DhcI00iX+0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HNPlopNLk9hM6xZdRZkZFW+dSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ywaZWWDYWGWVjUTR939+J399roD1B0y2+PpxxVJkES/1Y+Zj0+-----END CERTIFICATE-----++DigiCert Assured ID Root G2+===========================+# Fingerprint (SHA-256): 7D:05:EB:B6:82:33:9F:8C:94:51:EE:09:4E:EB:FE:FA:79:53:A1:14:ED:B2:F4:49:49:45:2F:AB:7D:2F:C1:85+-----BEGIN CERTIFICATE-----+MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQG+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw+IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgw+MTE1MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL+ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIw+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSAn61UQbVH+35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4HteccbiJVMWWXvdMX0h5i89vq+bFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9HpEgjAALAcKxHad3A2m67OeYfcgnDmCXRw+VWmvo2ifv922ebPynXApVfSr/5Vh88lAbx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OP+YLfykqGxvYmJHzDNw6YuYjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+Rn+lTGNAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTO+w0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPIQW5pJ6d1Ee88hjZv+0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I0jJmwYrA8y8678Dj1JGG0VDjA9tz+d29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4GnilmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAW+hsI6yLETcDbYz+70CjTVW0z9B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0M+jomZmWzwPDCvON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo+IhNzbM8m9Yop5w==+-----END CERTIFICATE-----++DigiCert Assured ID Root G3+===========================+# Fingerprint (SHA-256): 7E:37:CB:8B:4C:47:09:0C:AB:36:55:1B:A6:F4:5D:B8:40:68:0F:BA:16:6A:95:2D:B1:00:71:7F:43:05:3F:C2+-----BEGIN CERTIFICATE-----+MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV+UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYD+VQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1+MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3+d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQ+BgcqhkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJfZn4f5dwb+RXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17QRSAPWXYQ1qAk8C3eNvJs+KTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgF+UaFNN6KDec6NHSrkhDAKBggqhkjOPQQDAwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5Fy+YZ5eEJJZVrmDxxDnOOlYJjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy+1vUhZscv6pZjamVFkpUBtA==+-----END CERTIFICATE-----++DigiCert Global Root G2+=======================+# Fingerprint (SHA-256): CB:3C:CB:B7:60:31:E5:E0:13:8F:8D:D3:9A:23:F9:DE:47:FF:C3:5E:43:C1:14:4C:EA:27:D4:6A:5A:B1:CB:5F+-----BEGIN CERTIFICATE-----+MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQG+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw+HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUx+MjAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3+dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkq+hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI2/Ou8jqJ+kTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx1x7e/dfgy5SDN67sH0NO+3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQq2EGnI/yuum06ZIya7XzV+hdG82MHauV+BJVJ8zUtluNJbd134/tJS7SsVQepj5WztCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyM+UNGPHgm+F6HmIcr9g+UQvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQAB+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV5uNu+5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY1Yl9PMWLSn/pvtsr+F9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4NeF22d+mQrvHRAiGfzZ0JFrabA0U+WTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NGFdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBH+QRFXGU7Aj64GxJUTFy8bJZ918rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/+iyK5S9kJRaTepLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl+MrY=+-----END CERTIFICATE-----++DigiCert Global Root G3+=======================+# Fingerprint (SHA-256): 31:AD:66:48:F8:10:41:38:C7:38:F3:9E:A4:32:01:33:39:3E:3A:18:CC:02:29:6E:F9:7C:2A:C9:EF:67:31:D0+-----BEGIN CERTIFICATE-----+MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQswCQYDVQQGEwJV+UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYD+VQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAw+MDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5k+aWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0C+AQYFK4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FGfp4tn+6O+YwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPOZ9wj/wMco+I+o0IwQDAP+BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNp+Yim8S8YwCgYIKoZIzj0EAwMDaAAwZQIxAK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y+3maTD/HMsQmP3Wyr+mt/oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34+VOKa5Vt8sycX+-----END CERTIFICATE-----++DigiCert Trusted Root G4+========================+# Fingerprint (SHA-256): 55:2F:7B:DC:F1:A7:AF:9E:6C:E6:72:01:7F:4F:12:AB:F7:72:40:C7:8E:76:1A:C2:03:D1:D9:D2:0A:C8:99:88+-----BEGIN CERTIFICATE-----+MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQG+EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEw+HwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1+MTIwMDAwWjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3+d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0G+CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3yithZwuEp+pz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9o+k3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7Fsa+vOvJz82sNEBfsXpm7nfISKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGY+QJB5w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6+MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCiEhtm+mnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7+f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFH+dL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8+oR7FwI+isX4KJpn15GkvmB0t9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud+DwEB/wQEAwIBhjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD+ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2SV1EY+CtnJYY+ZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd+SeuMIW59mdNOj6PWTkiU0Tr+yF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWcfFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy+7zBZLq7gcfJW5GqXb5JQbZaNaHqasjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iah+ixTXTBmyUEFxPT9NcCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN+5r5N0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie4u1Ki7wb+/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mIr/OSmbaz5mEP0oUA51Aa+5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tK+G48BtieVU+i2iW1bvGjUI+iLUaJW+fCmgKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP+82Z++-----END CERTIFICATE-----++COMODO RSA Certification Authority+==================================+# Fingerprint (SHA-256): 52:F0:E1:C4:E5:8E:C6:29:29:1B:60:31:7F:07:46:71:B8:5D:7E:A8:0D:5B:07:27:34:63:53:4B:32:B4:02:34+-----BEGIN CERTIFICATE-----+MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCBhTELMAkGA1UE+BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG+A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlv+biBBdXRob3JpdHkwHhcNMTAwMTE5MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMC+R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE+ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBB+dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR6FSS0gpWsawNJN3Fz0Rn+dJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8Xpz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZ+FGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt++5eNu/Nio5JIk2kNrYrhV/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pG+x8cgoLEfZd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z+pUX+2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7wqP/0uK3pN/u6uPQL+OvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZahSL0896+1DSJMwBGB7FY79tOi4lu3+sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVICu9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+C+GCe01a60y1Dma/RMhnEw6abfFobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5+WdYgGq/yapiqcrxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E+FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8w+DQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvlwFTPoCWOAvn9sKIN9SCYPBMt+rFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj++nq6PK7o9mfjYcwlYRm6mnPTXJ9OV2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSg+tZx8jb8uk2IntznaFxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwW+sRqZCuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiKboHGhfKp+pC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmckejkk9u+UJueBPSZI9FoJA+zMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yLS0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHq+ZJx64SIDqZxubw5lT2yHh17zbqD5daWbQOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk52+7RH89elWsn2/x20Kk4yl0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7I+LaZRfyHBNVOFBkpdn627G190+-----END CERTIFICATE-----++USERTrust RSA Certification Authority+=====================================+# Fingerprint (SHA-256): E7:93:C9:B0:2F:D8:AA:13:E2:1C:31:22:8A:CC:B0:81:19:64:3B:74:9C:89:89:64:B1:74:6D:46:C3:D4:CB:D2+-----BEGIN CERTIFICATE-----+MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCBiDELMAkGA1UE+BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK+ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh+dGlvbiBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UE+BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK+ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh+dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCAEmUXNg7D2wiz+0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2j+Y0K2dvKpOyuR+OJv0OwWIJAJPuLodMkYtJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFn+RghRy4YUVD+8M/5+bJz/Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O++T23LLb2VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT79uq+/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6c0Plfg6lZrEpfDKE+Y1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmTYo61Zs8liM2EuLE/pDkP2QKe6xJM+lXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97lc6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8+yexDJtC/QV9AqURE9JnnV4eeUB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6++eLf8ZxXhyVeEHg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd+BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF+MAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPFUp/L+M+ZBn8b2kMVn54CVVeW+FPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KOVWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ+7l8wXEskEVX/JJpuXior7gtNn3/3ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQ+Eg9zKC7F4iRO/Fjs8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM+8WcRiQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYzeSf7dNXGi+FSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZXHlKYC6SQK5MNyosycdi+yA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9c+J2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRBVXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGw+sAvgnEzDHNb842m1R0aBL6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gx+Q+6IHdfGjjxDah2nGN59PRbxYvnKkKj9+-----END CERTIFICATE-----++USERTrust ECC Certification Authority+=====================================+# Fingerprint (SHA-256): 4F:F4:60:D5:4B:9C:86:DA:BF:BC:FC:57:12:E0:40:0D:2B:ED:3F:BC:4D:4F:BD:AA:86:E0:6A:DC:D2:A9:AD:7A+-----BEGIN CERTIFICATE-----+MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDELMAkGA1UEBhMC+VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU+aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv+biBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMC+VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU+aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv+biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqfloI+d61SRvU8Za2EurxtW2+0eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinngo4N+LZfQYcTxmdwlkWOrfzCjtHDix6Ez+nPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0GA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNV+HQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBB+HU6+4WMBzzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbWRNZu+9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg=+-----END CERTIFICATE-----++GlobalSign ECC Root CA - R5+===========================+# Fingerprint (SHA-256): 17:9F:BC:14:8A:3D:D0:0F:D2:4E:A1:34:58:CC:43:BF:A7:F5:9C:81:82:D7:83:A5:13:F6:EB:EC:10:0C:89:24+-----BEGIN CERTIFICATE-----+MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEkMCIGA1UECxMb+R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD+EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb+R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD+EwpHbG9iYWxTaWduMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6+SFkc8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8kehOvRnkmS+h5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd+BgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYIKoZIzj0EAwMDaAAwZQIxAOVpEslu28Yx+uglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7+yFz9SO8NdCKoCOJuxUnOxwy8p2Fp8fc74SrL+SvzZpA3+-----END CERTIFICATE-----++IdenTrust Commercial Root CA 1+==============================+# Fingerprint (SHA-256): 5D:56:49:9B:E4:D2:E0:8B:CF:CA:D0:8A:3E:38:72:3D:50:50:3B:DE:70:69:48:E4:2F:55:60:30:19:E5:28:AE+-----BEGIN CERTIFICATE-----+MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBKMQswCQYDVQQG+EwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBS+b290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQwMTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzES+MBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENB+IDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ld+hNlT3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU+ehcCuz/+mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gpS0l4PJNgiCL8mdo2yMKi+1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1bVoE/c40yiTcdCMbXTMTEl3EASX2MN0C+XZ/g1Ue9tOsbobtJSdifWwLziuQkkORiT0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl+3ZBWzvurpWCdxJ35UrCLvYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzy+NeVJSQjKVsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZKdHzV+WYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHTc+XvvqDtMwt0viAg+xGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hvl7yTmvmcEpB4eoCHFddydJxVdHix+uuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5NiGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMC+AQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZI+hvcNAQELBQADggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH+6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwtLRvM7Kqas6pg+ghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93nAbowacYXVKV7cndJZ5t+qnt+ozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmV+YjzlVYA211QC//G5Xc7UI2/YRYRKW2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUX+feu+h1sXIFRRk0pTAwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/ro+kTLql1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG4iZZRHUe+2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZmUlO+KWA2yUPHGNiiskz+Z2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7R+cGzM7vRX+Bi6hG6H+-----END CERTIFICATE-----++IdenTrust Public Sector Root CA 1+=================================+# Fingerprint (SHA-256): 30:D0:89:5A:9A:44:8A:26:20:91:63:55:22:D1:F5:20:10:B5:86:7A:CA:E1:2C:78:EF:95:8F:D4:F4:38:9F:2F+-----BEGIN CERTIFICATE-----+MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBNMQswCQYDVQQG+EwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3Rv+ciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcNMzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJV+UzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBS+b290IENBIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTy+P4o7ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGyRBb06tD6+Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlSbdsHyo+1W/CD80/HLaXI+rcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF/YTLNiCBWS2ab21ISGHKTN9T0a9SvESf+qy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoS+mJxZZoY+rfGwyj4GD3vwEUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFn+ol57plzy9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9VGxyh+LrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ2fjXctscvG29ZV/v+iDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsVWaFHVCkugyhfHMKiq3IXAAaOReyL+4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gDW/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8B+Af8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMw+DQYJKoZIhvcNAQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj+t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHVDRDtfULAj+7A+mgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9TaDKQGXSc3z1i9kKlT/YPyNt+GtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8GlwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFt+m6/n6J91eEyrRjuazr8FGF1NFTwWmhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMx+NRF4eKLg6TCMf4DfWN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4+Mhn5+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJtshquDDI+ajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhAGaQdp/lLQzfcaFpPz+vC+ZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ+3Wl9af0AVqW3rLatt8o+Ae+c+-----END CERTIFICATE-----++CFCA EV ROOT+============+# Fingerprint (SHA-256): 5C:C3:D7:8E:4E:1D:5E:45:54:7A:04:E6:87:3E:64:F9:0C:F9:53:6D:1C:CC:2E:F8:00:F3:55:C4:C5:FD:70:FD+-----BEGIN CERTIFICATE-----+MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjEwMC4GA1UE+CgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNB+IEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkxMjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEw+MC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQD+DAxDRkNBIEVWIFJPT1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnV+BU03sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpLTIpTUnrD+7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5/ZOkVIBMUtRSqy5J35DN+uF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp7hZZLDRJGqgG16iI0gNyejLi6mhNbiyW+ZXvKWfry4t3uMCz7zEasxGPrb382KzRzEpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7+xzbh72fROdOXW3NiGUgthxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9f+py25IGvPa931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqotaK8K+gWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNgTnYGmE69g60dWIol+hdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfVPKPtl8MeNPo4+QgO48BdK4PRVmrJ+tqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hvcWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAf+BgNVHSMEGDAWgBTj/i39KNALtbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB+/wQEAwIBBjAdBgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB+ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObTej/tUxPQ4i9q+ecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdLjOztUmCypAbqTuv0axn96/Ua+4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBSESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sG+E5uPhnEFtC+NiWYzKXZUmhH4J/qyP5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfX+BDrDMlI1Dlb4pd19xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjn+aH9dCi77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN5mydLIhy+PDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe/v5WOaHIz16eGWRGENoX+kbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+ZAAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3C+ekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su+-----END CERTIFICATE-----++OISTE WISeKey Global Root GB CA+===============================+# Fingerprint (SHA-256): 6B:9C:08:E8:6E:B0:F7:67:CF:AD:65:CD:98:B6:21:49:E5:49:4A:67:F5:84:5E:7B:D1:ED:01:9F:27:B8:6B:D6+-----BEGIN CERTIFICATE-----+MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBtMQswCQYDVQQG+EwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl+ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAw+MzJaFw0zOTEyMDExNTEwMzFaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYD+VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEds+b2JhbCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3HEokKtaX+scriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGxWuR51jIjK+FTzJlFXHtP+rby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk+9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNku7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4o+Qnc/nSMbsrY9gBQHTC5P99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvg+GUpuuy9rM2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB+/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZI+hvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrghcViXfa43FK8+5/ea4n32cZiZBKpD+dHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0+VQreUGdNZtGn//3ZwLWoo4rOZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEui+HZeeevJuQHHfaPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic+Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM=+-----END CERTIFICATE-----++SZAFIR ROOT CA2+===============+# Fingerprint (SHA-256): A1:33:9D:33:28:1A:0B:56:E5:57:D3:D3:2B:1C:E7:F9:36:7E:B0:94:BD:5F:A7:2A:7E:50:04:C8:DE:D7:CA:FE+-----BEGIN CERTIFICATE-----+MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQELBQAwUTELMAkG+A1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6ZW5pb3dhIFMuQS4xGDAWBgNV+BAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkwNzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJ+BgNVBAYTAlBMMSgwJgYDVQQKDB9LcmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYD+VQQDDA9TWkFGSVIgUk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5Q+qEvNQLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT3PSQ1hNK+DJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw3gAeqDRHu5rr/gsUvTaE+2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr63fE9biCloBK0TXC5ztdyO4mTp4CEHCdJ+ckm1/zuVnsHMyAHs6A6KCpbns6aH5db5BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwi+ieDhZNRnvDF5YTy7ykHNXGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P+AQH/BAQDAgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsFAAOC+AQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw8PRBEew/R40/cof5+O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOGnXkZ7/e7DDWQw4rtTw/1zBLZpD67+oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCPoky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul+4+vJhaAlIDf7js4MNIThPIGyd05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6++/NNIxuZMzSgLvWpCz/UXeHPhJ/iGcJfitYgHuNztw==+-----END CERTIFICATE-----++Certum Trusted Network CA 2+===========================+# Fingerprint (SHA-256): B6:76:F2:ED:DA:E8:77:5C:D3:6C:B0:F6:3C:D1:D4:60:39:61:F4:9E:62:65:BA:01:3A:2F:03:07:B6:D0:B8:04+-----BEGIN CERTIFICATE-----+MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCBgDELMAkGA1UE+BhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNVBAsTHkNlcnR1+bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29y+ayBDQSAyMCIYDzIwMTExMDA2MDgzOTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQ+TDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENl+cnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENB+IDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWADGSdhhuWZGc/IjoedQF9+7/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+o+CgCXhVqqndwpyeI1B+twTUrWwbNWuKFBOJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40b+Rr5HMNUuctHFY9rnY3lEfktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2p+uTRZCr+ESv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1mo130+GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02isx7QBlrd9pPPV3WZ+9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOWOZV7bIBaTxNyxtd9KXpEulKkKtVB+Rgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgezTv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pye+hizKV/Ma5ciSixqClnrDvFASadgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vM+BhBgu4M1t15n3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD+AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZI+hvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQF/xlhMcQSZDe28cmk4gmb3DW+Al45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTfCVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuA+L55MYIR4PSFk1vtBHxgP58l1cb29XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMo+clm2q8KMZiYcdywmdjWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tM+pkT/WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jbAoJnwTnb+w3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksqP/ujmv5zMnHCnsZy4Ypo+J/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Kob7a6bINDd82Kkhehnlt4Fj1F4jNy3eFm+ypnTycUm/Q1oBEauttmbjL4ZvrHG8hnjXALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLX+is7VmFxWlgPF7ncGNf/P5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7+zAYspsbiDrW5viSP+-----END CERTIFICATE-----++Hellenic Academic and Research Institutions RootCA 2015+=======================================================+# Fingerprint (SHA-256): A0:40:92:9A:02:CE:53:B4:AC:F4:F2:FF:C6:98:1C:E4:49:6F:75:5E:6D:45:FE:0B:2A:69:2B:CD:52:52:3F:36+-----BEGIN CERTIFICATE-----+MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcT+BkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0+aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl+YXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAx+MTIxWjCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMg+QWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNV+BAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIw+MTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDC+Kk/G4n8PDwEXT2QNrCROnk8Zlrv+bTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+eh+iGsxr/CL0BgzuNtFajT0AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts++6PAQZe104S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06CojXd+FPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV9Cz82XBST3i4vTwr+i5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrDgfgXy5I2XdGj2HUb4Ysn6npIQf1F+GQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2+fu/Z8VFRfS0myGlZYeCsargqNhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9mu+iNX6hME6wGkoLfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc+Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD+AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVdctA4GGqd83EkVAswDQYJKoZI+hvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0IXtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF++D1hYc2Ryx+hFjtyp8iY/xnmMsVMIM4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrM+d/K4kPFox/la/vot9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+y+d+2VZ5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/eaj8GsGsVn+82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnhX9izjFk0WaSrT2y7Hxjb+davYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQl033DlZdwJVqwjbDG2jJ9SrcR5q+ss7F+Jej6A7na+RZukYT1HCjI/CbM1xyQVqdfbzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVt+J94Cj8rDtSvK6evIIVM4pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGa+JI7ZjnHKe7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0vm9q+p/UsQu0yrbYhnr68+-----END CERTIFICATE-----++Hellenic Academic and Research Institutions ECC RootCA 2015+===========================================================+# Fingerprint (SHA-256): 44:B5:45:AA:8A:25:E6:5A:73:CA:15:DC:27:FC:36:D2:4C:1C:B9:95:3A:06:65:39:B1:15:82:DC:48:7B:48:33+-----BEGIN CERTIFICATE-----+MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0+aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9u+cyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj+aCBJbnN0aXR1dGlvbnMgRUNDIFJvb3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEw+MzcxMlowgaoxCzAJBgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmlj+IEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUQwQgYD+VQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIEVDQyBSb290+Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKgQehLgoRc4vgxEZmGZE4JJS+dQS8KrjVP+dJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJajq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoK+Vlp8aQuqgAkkbH7BRqNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O+BBYEFLQiC4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaeplSTA+GiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7SofTUwJCA3sS61kFyjn+dc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR+-----END CERTIFICATE-----++ISRG Root X1+============+# Fingerprint (SHA-256): 96:BC:EC:06:26:49:76:F3:74:60:77:9A:CF:28:C5:A7:CF:E8:A3:C0:AA:E1:1A:8F:FC:EE:05:C0:BD:DF:08:C6+-----BEGIN CERTIFICATE-----+MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UE+BhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQD+EwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQG+EwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMT+DElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54r+Vygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj1+3Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8K+b4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCN+Aymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ+4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf+1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFu+hjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQH+usEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/r+OPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4G+A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY+9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL+ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV+0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwt+hDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJw+TdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nx+e5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZA+JzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahD+YVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9n+JEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJ+m+kXQ99b21/+jh5Xos1AnX5iItreGCc=+-----END CERTIFICATE-----++AC RAIZ FNMT-RCM+================+# Fingerprint (SHA-256): EB:C5:57:0C:29:01:8C:4D:67:B1:AA:12:7B:AF:12:F7:03:B4:61:1E:BC:17:B7:DA:B5:57:38:94:17:9B:93:FA+-----BEGIN CERTIFICATE-----+MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsxCzAJBgNVBAYT+AkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTAeFw0wODEw+MjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJD+TTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC+ggIBALpxgHpMhm5/yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcf+qQgfBBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAzWHFctPVr+btQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxFtBDXaEAUwED653cXeuYL+j2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z374jNUUeAlz+taibmSXaXvMiwzn15Cou+08YfxGyqxRxqAQVKL9LFwag0Jl1mpdICIfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mw+WsXmo8RZZUc1g16p6DULmbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnT+tOmlcYF7wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peSMKGJ+47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2ZSysV4999AeU14EC+ll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMetUqIJ5G+GR4of6ygnXYMgrwTJbFaa+i0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE+FPd9xf3E6Jobd2Sn9R2gzL+HYJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1o+dHRwOi8vd3d3LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD+nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1RXxlDPiyN8+s+D8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYMLVN0V2Ue1bLdI4E7pWYjJ2cJ+j+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrT+Qfv6MooqtyuGC2mDOL7Nii4LcK2NJpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW++YJF1DngoABd15jmfZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7+Ixjp6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp1txyM/1d+8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B9kiABdcPUXmsEKvU7ANm+5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wokRqEIr9baRRmW1FMdW4R58MD3R++Lj8UG+rp1MYp3/RgT408m2ECVAdf4WqslKYIYvuu8wd+RU4riEmViAqhOLUTpPSPaLtrM=+-----END CERTIFICATE-----++Amazon Root CA 1+================+# Fingerprint (SHA-256): 8E:CD:E6:88:4F:3D:87:B1:12:5B:A3:1A:C3:FC:B1:3D:70:16:DE:7F:57:CC:90:4F:E1:CB:97:C6:AE:98:19:6E+-----BEGIN CERTIFICATE-----+MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsFADA5MQswCQYD+VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAxMB4XDTE1+MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv+bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC+ggEBALJ4gHHKeNXjca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgH+FzZM9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qwIFAGbHrQ+gLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6VOujw5H5SNz/0egwLX0t+dHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L93FcXmn/6pUCyziKrlA4b9v7LWIbxcce+VOF34GfID5yHI9Y/QCB/IIDEgEw+OyQmjgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB+/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3+DQEBCwUAA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDIU5PM+CCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUsN+gDS63pYaACbvXy+8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vvo/ufQJVtMVT8QtPHRh8jrdkPSHCa+2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2+xJNDd2ZhwLnoQdeXeGADbkpyrqXRfboQnoZsG4q5WTP468SQvvG5+-----END CERTIFICATE-----++Amazon Root CA 2+================+# Fingerprint (SHA-256): 1B:A5:B2:AA:8C:65:40:1A:82:96:01:18:F8:0B:EC:4F:62:30:4D:83:CE:C4:71:3A:19:C3:9C:01:1E:A4:6D:B4+-----BEGIN CERTIFICATE-----+MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwFADA5MQswCQYD+VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAyMB4XDTE1+MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv+bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC+ggIBAK2Wny2cSkxKgXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4+kHbZW0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg1dKmSYXp+N+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K8nu+NQWpEjTj82R0Yiw9+AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvd+fLC6HM783k81ds8P+HgfajZRRidhW+mez/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAEx+kv8LV/SasrlX6avvDXbR8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSS+btqDT6ZjmUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz7Mt0+Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6+XUyo05f7O0oYtlN+c/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI0u1ufm8/0i2BWSlmy5A5lREedCf++3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSw+DPBMMPQFWAJI/TPlUq9LhONmUjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oA+A7CXDpO8Wqj2LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kSk5Nrp+gvU5LE+YFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl7uxMMne0nxrpS10gxdr9HIcW+xkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygmbtmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQ+gj9sAq+uEjonljYE1x2igGOpm/HlurR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbW+aQbLU8uz/mtBzUF+fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoV+Yh63n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE76KlXIx3+KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H9jVlpNMKVv/1F2Rs76gi+JUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT4PsJYGw=+-----END CERTIFICATE-----++Amazon Root CA 3+================+# Fingerprint (SHA-256): 18:CE:6C:FE:7B:F1:4E:60:B2:E3:47:B8:DF:E8:68:CB:31:D0:2E:BB:3A:DA:27:15:69:F5:03:43:B4:6D:B3:A4+-----BEGIN CERTIFICATE-----+MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5MQswCQYDVQQG+EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAzMB4XDTE1MDUy+NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ+MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZB+f8ANm+gBG1bG8lKlui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjr+Zt6jQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSrttvXBp43+rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkrBqWTrBqYaGFy+uGh0Psc+eGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteMYyRIHN8wfdVoOw==+-----END CERTIFICATE-----++Amazon Root CA 4+================+# Fingerprint (SHA-256): E3:5D:28:41:9E:D0:20:25:CF:A6:90:38:CD:62:39:62:45:8D:A5:C6:95:FB:DE:A3:C2:2B:0B:FB:25:89:70:92+-----BEGIN CERTIFICATE-----+MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5MQswCQYDVQQG+EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSA0MB4XDTE1MDUy+NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ+MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN+/sGKe0uoe0ZLY7Bi9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri+83BkM6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV+HQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WBMAoGCCqGSM49BAMDA2gA+MGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlwCkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1+AE47xDqUEpHJWEadIRNyp4iciuRMStuW1KyLa2tJElMzrdfkviT8tQp21KW8EA==+-----END CERTIFICATE-----++TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1+=============================================+# Fingerprint (SHA-256): 46:ED:C3:68:90:46:D5:3A:45:3F:B3:10:4A:B8:0D:CA:EC:65:8B:26:60:EA:16:29:DD:7E:86:79:90:64:87:16+-----BEGIN CERTIFICATE-----+MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIxGDAWBgNVBAcT+D0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxpbXNlbCB2ZSBUZWtub2xvamlr+IEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0wKwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24g+TWVya2V6aSAtIEthbXUgU00xNjA0BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRp+ZmlrYXNpIC0gU3VydW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYD+VQQGEwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXllIEJpbGlt+c2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklUQUsxLTArBgNVBAsTJEth+bXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBTTTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11+IFNNIFNTTCBLb2sgU2VydGlmaWthc2kgLSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A+MIIBCgKCAQEAr3UwM6q7a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y8+6Ij5iySrLqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INrN3wc+wv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2XYacQuFWQfw4tJzh0+3+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/iSIzL+aFCr2lqBs23tPcLG07xxO9+WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4fAJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQU+ZT/HiobGPN08VFw1+DrtUgxHV8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJ+KoZIhvcNAQELBQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh+AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPfIPP54+M638yc+lNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4lzwDGrpDxpa5RXI4s6ehlj2R+e37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0j+q5Rm+K37DwhuJi1/FwcJsoz7UMCflo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM=+-----END CERTIFICATE-----++GDCA TrustAUTH R5 ROOT+======================+# Fingerprint (SHA-256): BF:FF:8F:D0:44:33:48:7D:6A:8A:A6:0C:1A:29:76:7A:9F:C2:BB:B0:5E:42:0F:71:3A:13:B9:92:89:1D:38:93+-----BEGIN CERTIFICATE-----+MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCQ04xMjAw+BgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8wHQYDVQQD+DBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVow+YjELMAkGA1UEBhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ+IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0B+AQEFAAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJjDp6L3TQs+AlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBjTnnEt1u9ol2x8kECK62p+OqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+uKU49tm7srsHwJ5uu4/Ts765/94Y9cnrr+pftZTqfrlYwiOXnhLQiPzLyRuEH3FMEjqcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ+9Cy5WmYqsBebnh52nUpmMUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQ+xXABZG12ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloPzgsM+R6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3GkL30SgLdTMEZeS1SZ+D2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeCjGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4+oR24qoAATILnsn8JuLwwoC8N9VKejveSswoAHQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx+9hoh49pwBiFYFIeFd3mqgnkCAwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlR+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg+p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZmDRd9FBUb1Ov9+H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5COmSdI31R9KrO9b7eGZONn35+6ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ryL3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd++PwyvzeG5LuOmCd+uh8W4XAR8gPfJWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQ+HtZa37dG/OaG+svgIHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBD+F8Io2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV09tL7ECQ+8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQXR4EzzffHqhmsYzmIGrv+/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrqT8p+ck0LcIymSLumoRT2+1hEmRSuqguT+aaApJUqlyyvdimYHFngVV3Eb7PVHhPOeMTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g==+-----END CERTIFICATE-----++SSL.com Root Certification Authority RSA+========================================+# Fingerprint (SHA-256): 85:66:6A:56:2E:E0:BE:5C:E9:25:C1:D8:89:0A:6F:76:A8:7E:C1:6D:4D:7D:5F:29:EA:74:19:CF:20:12:3B:69+-----BEGIN CERTIFICATE-----+MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxDjAM+BgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24x+MTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYw+MjEyMTczOTM5WhcNNDEwMjEyMTczOTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx+EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NM+LmNvbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcNAQEBBQAD+ggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2RxFdHaxh3a3by/ZPkPQ/C+Fp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aXqhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8+P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcCC52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/ge+oeOy3ZExqysdBP+lSgQ36YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkp+k8zruFvh/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrFYD3Z+fBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93EJNyAKoFBbZQ+yODJ+gUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVcUS4cK38acijnALXRdMbX5J+tB5O2+UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi8+1xtZPCvM8hnIk2snYxnP/Okm+Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4s+bE6x/c+cCbqiM+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV+HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4GA1UdDwEB/wQE+AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGVcpNxJK1ok1iOMq8bs3AD/CUr+dIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBcHadm47GUBwwyOabqG7B52B2ccETjit3E+ZUf+ijhDPwGFpUenPUayvOUiaPd7nNgsPgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAsl+u1OJD7OAUN5F7kR/q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjq+erQ0cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jra6x+3uxj+MxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90IH37hVZkLId6Tngr75qNJ+vTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/YK9f1JmzJBjSWFupwWRoyeXkLtoh/D1JI+Pb9s2KJELtFOt3JY04kTlf5Eq/jXixtunLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406y+wKBjYZC6VWg3dGq2ktufoYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NI+WuuA8ShYIc2wBlX7Jz9TkHCpBB5XJ7k=+-----END CERTIFICATE-----++SSL.com Root Certification Authority ECC+========================================+# Fingerprint (SHA-256): 34:17:BB:06:CC:60:07:DA:1B:96:1C:92:0B:8A:B4:CE:3F:AD:82:0E:4A:A3:0B:9A:CB:C4:A7:4E:BD:CE:BC:65+-----BEGIN CERTIFICATE-----+MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMCVVMxDjAMBgNV+BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xMTAv+BgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEy+MTgxNDAzWhcNNDEwMjEyMTgxNDAzWjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAO+BgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv+bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuBBAAiA2IA+BEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI7Z4INcgn64mMU1jrYor++8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPgCemB+vNH06NjMGEwHQYDVR0OBBYEFILR+hXMw5zUE044CkvvlpNHEIejNMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTT+jgKS++Wk0cQh6M0wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCW+e+0F+S8Tkdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+gA0z+5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl+-----END CERTIFICATE-----++SSL.com EV Root Certification Authority RSA R2+==============================================+# Fingerprint (SHA-256): 2E:7B:F1:6C:C2:24:85:A7:BB:E2:AA:86:96:75:07:61:B0:AE:39:BE:3B:2F:E9:D0:CC:6D:4E:F7:34:91:42:5C+-----BEGIN CERTIFICATE-----+MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNVBAYTAlVTMQ4w+DAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9u+MTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy+MB4XDTE3MDUzMTE4MTQzN1oXDTQyMDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQI+DAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYD+VQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMIICIjAN+BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvqM0fNTPl9fb69LT3w23jh+hqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssufOePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7w+cXHswxzpY6IXFJ3vG2fThVUCAtZJycxa4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTO+Zw+oz12WGQvE43LrrdF9HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw++B6KjBSYRaZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcAb9Zh+CBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQGp8hLH94t2S42Oim+9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQVPWKchjgGAGYS5Fl2WlPAApiiECto+RHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMOpgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+Slm+JuwgUHfbSguPvuUCYHBBXtSuUDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48++qvWBkofZ6aYMBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV+HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa49QaAJadz20Zp+qJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBWs47LCp1Jjr+kxJG7ZhcFUZh1+++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nx+Y/hoLVUE0fKNsKTPvDxeH3jnpaAgcLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2G+guDKBAdRUNf/ktUM79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDz+OFSz/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXtll9ldDz7+CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEmKf7GUmG6sXP/wwyc5Wxq+lD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKKQbNmC1r7fSOl8hqw/96bg5Qu0T/fkreR+rwU7ZcegbLHNYhLDkBvjJc40vG93drEQw/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1+hlMYegouCRw2n5H9gooiS9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX+9hwJ1C07mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w==+-----END CERTIFICATE-----++SSL.com EV Root Certification Authority ECC+===========================================+# Fingerprint (SHA-256): 22:A2:C1:F7:BD:ED:70:4C:C1:E7:01:B5:F4:08:C3:10:88:0F:E9:56:B5:DE:2A:4A:44:F9:9C:87:3A:25:A7:C8+-----BEGIN CERTIFICATE-----+MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMCVVMxDjAMBgNV+BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xNDAy+BgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYw+MjEyMTgxNTIzWhcNNDEwMjEyMTgxNTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx+EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NM+LmNvbSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB+BAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMAVIbc/R/fALhBYlzccBYy+3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1KthkuWnBaBu2+8KGwytAJKaNjMGEwHQYDVR0O+BBYEFFvKXuXe0oGqzagtZFG22XKbl+ZPMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe+5d7SgarNqC1kUbbZcpuX5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJ+N+vp1RPZytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZgh5Mm+m7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg==+-----END CERTIFICATE-----++GlobalSign Root CA - R6+=======================+# Fingerprint (SHA-256): 2C:AB:EA:FE:37:D0:6C:A2:2A:BA:73:91:C0:03:3D:25:98:29:52:C4:53:64:73:49:76:3A:3A:B5:AD:6C:CF:69+-----BEGIN CERTIFICATE-----+MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEgMB4GA1UECxMX+R2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkds+b2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQxMjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9i+YWxTaWduIFJvb3QgQ0EgLSBSNjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFs+U2lnbjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQss+grRIxutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1kZguSgMpE+3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxDaNc9PIrFsmbVkJq3MQbF+vuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJwLnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqM+PKq0pPbzlUoSB239jLKJz9CgYXfIWHSw1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi++azayOeSsJDa38O+2HBNXk7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05O+WgtH8wY2SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/hbguy+CLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4nWUx2OVvq+aWh2IMP+0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpYrZxCRXluDocZXFSxZba/jJvcE+kN+b7gu3GduyYsRtYQUigAZcIN5kZeR1BonvzceMgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQE+AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNV+HSMEGDAWgBSubAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN+nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGtIxg93eFyRJa0+lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr6155wsTLxDKZmOMNOsIeDjHfrY+BzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLjvUYAGm0CuiVdjaExUd1URhxN25mW7xocBFym+Fe944Hn+Xds+qkxV/ZoVqW/hpvvfcDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr+3TsTjxKM4kEaSHpzoHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB1+0jZpnOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfspA9MRf/T+uTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+vJJUEeKgDu+6B5dpffItK+oZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+t+JDfLRVpOoERIyNiwmcUVhAn21klJwGW45hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA=+-----END CERTIFICATE-----++OISTE WISeKey Global Root GC CA+===============================+# Fingerprint (SHA-256): 85:60:F9:1C:36:24:DA:BA:95:70:B5:FE:A0:DB:E3:6F:F1:1A:83:23:BE:94:86:85:4F:B3:F3:4A:55:71:19:8D+-----BEGIN CERTIFICATE-----+MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQswCQYDVQQGEwJD+SDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNlZDEo+MCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRa+Fw00MjA1MDkwOTU4MzNaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQL+ExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh+bCBSb290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4nieUqjFqdr+VCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4Wp2OQ0jnUsYd4XxiWD1Ab+NTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd+BgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7TrYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0E+AwMDaAAwZQIwJsdpW9zV57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtk+AjEA2zQgMgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9+-----END CERTIFICATE-----++UCA Global G2 Root+==================+# Fingerprint (SHA-256): 9B:EA:11:C9:76:FE:01:47:64:C1:BE:56:A6:F9:14:B5:A5:60:31:7A:BD:99:88:39:33:82:E5:16:1A:A0:49:3C+-----BEGIN CERTIFICATE-----+MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQG+EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBHbG9iYWwgRzIgUm9vdDAeFw0x+NjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0xCzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlU+cnVzdDEbMBkGA1UEAwwSVUNBIEdsb2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A+MIICCgKCAgEAxeYrb3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmT+oni9kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzmVHqUwCoV+8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/RVogvGjqNO7uCEeBHANBS+h6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDcC/Vkw85DvG1xudLeJ1uK6NjGruFZfc8o+LTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIjtm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/+R+zvWr9LesGtOxdQXGLYD0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBe+KW4bHAyvj5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6DlNaBa+4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6iIis7nCs+dwp4wwc+OxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznPO6Q0ibd5Ei9Hxeepl2n8pndntd97+8XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O+BBYEFIHEjMz15DD/pQwIX4wVZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo+5sOASD0Ee/ojL3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5+1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl1qnN3e92mI0A+Ds0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oUb3n09tDh05S60FdRvScFDcH9+yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LVPtateJLbXDzz2K36uGt/xDYotgIVilQsnLAX+c47QN6MUPJiVAAwpBVueSUmxX8fjy88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHo+jhJi6IjMtX9Gl8CbEGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZk+bxqgDMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI+Vg7RE+x+ygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGyYiGqhkCyLmTTX8jjfhFn+RR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bXUB+K+wb1whnw0A==+-----END CERTIFICATE-----++UCA Extended Validation Root+============================+# Fingerprint (SHA-256): D4:3A:F9:B3:54:73:75:5C:96:84:FC:06:D7:D8:CB:70:EE:5C:28:E7:73:FB:29:4E:B4:1E:E7:17:22:92:4D:24+-----BEGIN CERTIFICATE-----+MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBHMQswCQYDVQQG+EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9u+IFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMxMDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8G+A1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIi+MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrs+iWogD4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvSsPGP2KxF+Rv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aopO2z6+I9tTcg1367r3CTu+eUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dksHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR+59mzLC52LqGj3n5qiAno8geK+LLNEOfic0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH+0mK1lTnj8/FtDw5lhIpjVMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KR+el7sFsLzKuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/TuDv+B0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41Gsx2VYVdWf6/wFlth+WG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs1+lvK9JKBZP8nm9rZ/+I8U6laUpS+NwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQDfwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS+3H5aBZ8eNJr34RQwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQEL+BQADggIBADaNl8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR+ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQVBcZEhrxH9cM+aVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5c6sq1WnIeJEmMX3ixzDx/BR4+dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb++7lsq+KePRXBOy5nAliRn+/4Qh8st2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOW+F3sGPjLtx7dCvHaj2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwi+GpWOvpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2CxR9GUeOc+GMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmxcmtpzyKEC2IPrNkZAJSi+djzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbMfjKaiJUINlK73nZfdklJrX+9ZSCyycEr+dhh2n1ax+-----END CERTIFICATE-----++Certigna Root CA+================+# Fingerprint (SHA-256): D4:8D:3D:23:EE:DB:50:A4:59:E5:51:97:60:1C:27:77:4B:9D:7B:18:C9:4D:5A:05:95:11:A1:02:50:B9:31:68+-----BEGIN CERTIFICATE-----+MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAwWjELMAkGA1UE+BhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAwMiA0ODE0NjMwODEwMDAzNjEZ+MBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0xMzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjda+MFoxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYz+MDgxMDAwMzYxGTAXBgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4IC+DwAwggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sOty3tRQgX+stmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9MCiBtnyN6tMbaLOQdLNyz+KNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPuI9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8+JXrJhFwLrN1CTivngqIkicuQstDuI7pmTLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16+XdG+RCYyKfHx9WzMfgIhC59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq+4NYKpkDfePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3YzIoej+wpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWTCo/1VTp2lc5ZmIoJ+lXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1kJWumIWmbat10TWuXekG9qxf5kBdI+jzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp/+/TBt2dzhauH8XwIDAQABo4IBGjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw+HQYDVR0OBBYEFBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of+1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczovL3d3d3cuY2Vy+dGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilodHRwOi8vY3JsLmNlcnRpZ25h+LmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYraHR0cDovL2NybC5kaGlteW90aXMuY29tL2Nl+cnRpZ25hcm9vdGNhLmNybDANBgkqhkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOIt+OoldaDgvUSILSo3L6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxP+TGRGHVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH60BGM+RFq+7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncBlA2c5uk5jR+mUYyZDDl3+4bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdio2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd+8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS+6Cvu5zHbugRqh5jnxV/vfaci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaY+tlu3zM63Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayhjWZS+aX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw3kAP+HwV96LOPNde+E4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0=+-----END CERTIFICATE-----++emSign Root CA - G1+===================+# Fingerprint (SHA-256): 40:F6:AF:03:46:A9:9A:A1:CD:1D:55:5A:4E:9C:CE:62:C7:F9:63:46:03:EE:40:66:15:83:3D:C8:C8:D0:03:67+-----BEGIN CERTIFICATE-----+MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJJTjET+MBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl+ZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBHMTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgx+ODMwMDBaMGcxCzAJBgNVBAYTAklOMRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVk+aHJhIFRlY2hub2xvZ2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIB+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQzf2N4aLTN+LnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO8oG0x5ZOrRkVUkr+PHB1+cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aqd7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHW+DV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhMtTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ+6DqS0hdW5TUaQBw+jSztOd9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrH+hQIDAQABo0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQDAgEG+MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31xPaOfG1vR2vjTnGs2+vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjMwiI/aTvFthUvozXGaCocV685743Q+NcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6dGNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q++Mri/Tm3R7nrft8EI6/6nAYH6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeih+U80Bv2noWgbyRQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx+iN66zB+Afko=+-----END CERTIFICATE-----++Hongkong Post Root CA 3+=======================+# Fingerprint (SHA-256): 5A:2F:C0:3F:0C:83:B0:90:BB:FA:40:60:4B:09:88:44:6C:76:36:18:3D:F9:84:6E:17:10:1A:44:7F:B8:EF:D6+-----BEGIN CERTIFICATE-----+MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQELBQAwbzELMAkG+A1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJSG9uZyBLb25nMRYwFAYDVQQK+Ew1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25na29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2+MDMwMjI5NDZaFw00MjA2MDMwMjI5NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtv+bmcxEjAQBgNVBAcTCUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMX+SG9uZ2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz+iNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFOdem1p+/l6TWZ5Mwc50tf+jTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mIVoBc+L0sPOFMV4i707mV78vH9toxdCim+5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOe+sL4jpNrcyCse2m5FHomY2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj+0mRiikKYvLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+TtbNe/+JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZbx39ri1UbSsUgYT2u+y1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+l2oBlKN8W4UdKjk60FSh0Tlxnf0h++bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YKTE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsG+xVd7GYYKecsAyVKvQv83j+GjHno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwID+AQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e+i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEwDQYJKoZIhvcN+AQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG7BJ8dNVI0lkUmcDrudHr9Egw+W62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCkMpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWld+y8joRTnU+kLBEUx3XZL7av9YROXrgZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov++BS5gLNdTaqX4fnkGMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDc+eqFS3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJmOzj/2ZQw+9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+l6mc1X5VTMbeRRAc6uk7+nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6cJfTzPV4e0hz5sy229zdcxsshTrD3mUcY+hcErulWuBurQB7Lcq9CClnXO0lD+mefPL5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB+60PZ2Pierc+xYw5F9KBaLJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fq+dBb9HxEGmpv0+-----END CERTIFICATE-----++Microsoft ECC Root Certificate Authority 2017+=============================================+# Fingerprint (SHA-256): 35:8D:F3:9D:76:4A:F9:E1:B7:66:E9:C9:72:DF:35:2E:E1:5C:FA:C2:27:AF:6A:D1:D7:0E:8E:4A:6E:DC:BA:02+-----BEGIN CERTIFICATE-----+MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV+UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQgRUND+IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4+MjMxNjA0WjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw+NAYDVQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQ+BgcqhkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZRogPZnZH6+thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYbhGBKia/teQ87zvH2RPUB+eMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTIy5lycFIM++Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlf+Xu5gKcs68tvWMoQZP3zVL8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaR+eNtUjGUBiudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M=+-----END CERTIFICATE-----++Microsoft RSA Root Certificate Authority 2017+=============================================+# Fingerprint (SHA-256): C7:41:F7:0F:4B:2A:8D:88:BF:2E:71:C1:41:22:EF:53:EF:10:EB:A0:CF:A5:E6:4C:FA:20:F4:18:85:30:73:E0+-----BEGIN CERTIFICATE-----+MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBlMQswCQYDVQQG+EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQg+UlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIw+NzE4MjMwMDIzWjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u+MTYwNAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcw+ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZNt9GkMml+7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0ZdDMbRnMlfl7rEqUrQ7e+S0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw7+1VdyvD/IybLeS2v4I2wDwAW9lcfNcztmgGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI++dkC0zVJhUXAoP8XFWvLJjEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49F+yGcohJUcaDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaGYaRS+MLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6W6IYZVcSn2i51BVr+lMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4KUGsTuqwPN1q3ErWQgR5WrlcihtnJ+0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH+FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJ+ClTUFLkqqNfs+avNJVgyeY+QW5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYw+DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC+NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZCLgLNFgVZJ8og+6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OCgMNPOsduET/m4xaRhPtthH80+dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk++ONVFT24bcMKpBLBaYVu32TxU5nhSnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex+/2kskZGT4d9Mozd2TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDy+AmH3pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGRxpl/j8nW+ZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiAppGWSZI1b7rCoucL5mxAyE+7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKT+c0QWbej09+CVgI+WXTik9KveCjCHk9hNAHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D+5KbvtwEwXlGjefVwaaZBRA+GsCyRxj3qrg+E+-----END CERTIFICATE-----++e-Szigno Root CA 2017+=====================+# Fingerprint (SHA-256): BE:B0:0B:30:83:9B:9B:C3:2C:32:E4:44:79:05:95:06:41:F2:64:21:B1:5E:D0:89:19:8B:51:8A:E2:EA:1B:99+-----BEGIN CERTIFICATE-----+MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNVBAYTAkhVMREw+DwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUt+MjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJvb3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZa+Fw00MjA4MjIxMjA3MDZaMHExCzAJBgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UE+CgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3pp+Z25vIFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtvxie+RJCx+s1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+HWyx7xf58etqjYzBhMA8G+A1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSHERUI0arBeAyxr87GyZDv+vzAEwDAfBgNVHSMEGDAWgBSHERUI0arBeAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEA+tVfd14pVCzbhhkT61NlojbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxO+svxyqltZ+efcMQ==+-----END CERTIFICATE-----++certSIGN Root CA G2+===================+# Fingerprint (SHA-256): 65:7C:FE:2F:A7:3F:AA:38:46:25:71:F3:32:A2:36:3A:46:FC:E7:02:09:51:71:07:02:CD:FB:B6:EE:DA:33:05+-----BEGIN CERTIFICATE-----+MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNVBAYTAlJPMRQw+EgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjAeFw0xNzAy+MDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJBgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lH+TiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIP+ADCCAgoCggIBAMDFdRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05+N0IwvlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZuIt4Imfk+abBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhpn+Sc8CnTXPnGFiWeI8Mg+wT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKscpc/I1mbySKEwQdPzH/iV8oScLumZfNp+dWO9lfsbl83kqK/20U6o2YpxJM02PbyWxPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91Qqh+ngLjYl/rNUssuHLoPj1PrCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732+jcZZroiFDsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fxDTvf+95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgyLcsUDFDYg2WD7rlc+z8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6CeWRgKRM+o/1Pcmqr4tTluCRVLERL+iohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1Ud+DgQWBBSCIS1mxteg4BXrzkwJd8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOB+ywaK8SJJ6ejqkX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC+b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQlqiCA2ClV9+BB+/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0OJD7uNGzcgbJceaBxXntC6Z5+8hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+cNywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5+BiKDUyUM/FHE5r7iOZULJK2v0ZXkltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklW+atKcsWMy5WHgUyIOpwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tU+Sxfj03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZkPuXaTH4M+NMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE1LlSVHJ7liXMvGnjSG4N+0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MXQRBdJ3NghVdJIgc=+-----END CERTIFICATE-----++NAVER Global Root Certification Authority+=========================================+# Fingerprint (SHA-256): 88:F4:38:DC:F8:FF:D1:FA:8F:42:91:15:FF:E5:F8:2A:E1:E0:6E:0C:70:C3:75:FA:AD:71:7B:34:A4:9E:72:65+-----BEGIN CERTIFICATE-----+MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEMBQAwaTELMAkG+A1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRGT1JNIENvcnAuMTIwMAYDVQQD+DClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4+NDJaFw0zNzA4MTgyMzU5NTlaMGkxCzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVT+UyBQTEFURk9STSBDb3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlv+biBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVAiQqrDZBb+UGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH38dq6SZeWYp34+hInDEW++j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lEHoSTGEq0n+USZGnQJoViAbbJAh2+g1G7+XNr4rRVqmfeSVPc0W+m/6imBEtRTkZazkVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2+aacp+yPOiNgSnABIqKYPszuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4+Yb8ObtoqvC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHfnZ3z+VHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaGYQ5fG8Ir4ozVu53B+A0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo0es+nPxdGoMuK8u180SdOqcXYZai+cdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3aCJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejy+YhbLgGvtPe31HzClrkvJE+2KAQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNV+HQ4EFgQU0p+I36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB+Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoNqo0hV4/GPnrK+21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatjcu3cvuzHV+YwIHHW1xDBE1UB+jCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm+LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bx+hYTeodoS76TiEJd6eN4MUZeoIUCLhr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTg+E34h5prCy8VCZLQelHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTH+D8z7p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8piKCk5XQ+A76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLRLBT/DShycpWbXgnbiUSY+qqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oG+I/hGoiLtk/bdmuYqh7GYVPEi92tF4+KOdh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmg+kpzNNIaRkPpkUZ3+/uul9XXeifdy+-----END CERTIFICATE-----++AC RAIZ FNMT-RCM SERVIDORES SEGUROS+===================================+# Fingerprint (SHA-256): 55:41:53:B1:3D:2C:F9:DD:B7:53:BF:BE:1A:4E:0A:E0:8D:0A:A4:18:70:58:FE:60:A2:B8:62:B2:E4:B8:7B:CB+-----BEGIN CERTIFICATE-----+MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQswCQYDVQQGEwJF+UzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgwFgYDVQRhDA9WQVRFUy1RMjgy+NjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1SQ00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4+MTIyMDA5MzczM1oXDTQzMTIyMDA5MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQt+UkNNMQ4wDAYDVQQLDAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNB+QyBSQUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuBBAAiA2IA+BPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LHsbI6GA60XYyzZl2hNPk2+LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oKUm8BA06Oi6NCMEAwDwYDVR0TAQH/BAUw+AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqG+SM49BAMDA2kAMGYCMQCuSuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoD+zBOQn5ICMQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJyv+c=+-----END CERTIFICATE-----++GlobalSign Root R46+===================+# Fingerprint (SHA-256): 4F:A3:12:6D:8D:3A:11:D1:C4:85:5A:4F:80:7C:BA:D6:CF:91:9D:3A:5A:88:B0:3B:EA:2C:63:72:D9:3C:40:C9+-----BEGIN CERTIFICATE-----+MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUAMEYxCzAJBgNV+BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQDExNHbG9iYWxTaWduIFJv+b3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAX+BgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIi+MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08Es+CVeJOaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQGvGIFAha/+r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud316HCkD7rRlr+/fKYIje+2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo0q3v84RLHIf8E6M6cqJaESvWJ3En7YEt+bWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSEy132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvj+K8Cd+RTyG/FWaha/LIWFzXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD4+12lPFzYE+cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCNI/on+ccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzsx2sZy/N78CsHpdls+eVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqaByFrgY/bxFn63iLABJzjqls2k+g9+vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYD+VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEM+BQADggIBAHx47PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg+JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti2kM3S+LGteWy+gxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIkpnnpHs6i58FZFZ8d4kuaPp92+CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRFFRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZm+OUdkLG5NrmJ7v2B0GbhWrJKsFjLtrWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qq+JZ4d16GLuc1CLgSkZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwye+qiv5u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP4vkYxboz+nxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6N3ec592kD3ZDZopD8p/7+DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3vouXsXgxT7PntgMTzlSdriVZzH81Xwj3+QEUxeCp6+-----END CERTIFICATE-----++GlobalSign Root E46+===================+# Fingerprint (SHA-256): CB:B9:C4:4D:84:B8:04:3E:10:50:EA:31:A6:9F:51:49:55:D7:BF:D2:E2:C6:B4:93:01:01:9A:D6:1D:9F:50:58+-----BEGIN CERTIFICATE-----+MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYxCzAJBgNVBAYT+AkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQDExNHbG9iYWxTaWduIFJvb3Qg+RTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNV+BAoTEEdsb2JhbFNpZ24gbnYtc2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcq+hkjOPQIBBgUrgQQAIgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkB+jtjqR+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGddyXqBPCCj+QjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQxCpCPtsad0kRL+gLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZk+vLtoURMMA/cVi4RguYv/Uo7njLwcAjA8+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP++CAezNIm8BZ/3Hobui3A=+-----END CERTIFICATE-----++ANF Secure Server Root CA+=========================+# Fingerprint (SHA-256): FB:8F:EC:75:91:69:B9:10:6B:1E:51:16:44:C6:18:C5:13:04:37:3F:6C:06:43:08:8D:8B:EF:FD:1B:99:75:99+-----BEGIN CERTIFICATE-----+MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNVBAUTCUc2MzI4+NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lv+bjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNVBAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3Qg+Q0EwHhcNMTkwOTA0MTAwMDM4WhcNMzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEw+MQswCQYDVQQGEwJFUzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQw+EgYDVQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9vdCBDQTCC+AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCjcqQZAZ2cC4Ffc0m6p6zz+BE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9qyGFOtibBTI3/TO80sh9l2Ll49a2pcbnv+T1gdpd50IJeh7WhM3pIXS7yr/2WanvtH2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcv+B2VSAKduyK9o7PQUlrZXH1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXse+zx76W0OLzc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyRp1RM+VwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQzW7i1o0TJrH93PB0j+7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/SiOL9V8BY9KHcyi1Swr1+KuCLH5z+JTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJnLNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe+8TZBAQIvfXOn3kLMTOmJDVb3n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVO+Hj1tyRRM4y5Bu8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj+o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAOBgNVHQ8BAf8E+BAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEATh65isagmD9uw2nAalxJ+UqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzx+j6ptBZNscsdW699QIyjlRRA96Gejrw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDt+dD+4E5UGUcjohybKpFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM+5gf0vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjqOknkJjCb+5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ/zo1PqVUSlJZS2Db7v54+EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ92zg/LFis6ELhDtjTO0wugumDLmsx2d1H+hk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI+PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGy+g77FGr8H6lnco4g175x2MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3+r5+qPeoott7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw=+-----END CERTIFICATE-----++Certum EC-384 CA+================+# Fingerprint (SHA-256): 6B:32:80:85:62:53:18:AA:50:D1:73:C9:8D:8B:DA:09:D5:7E:27:41:3D:11:4C:F7:87:A0:F5:D0:6C:03:0C:F6+-----BEGIN CERTIFICATE-----+MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQswCQYDVQQGEwJQ+TDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2Vy+dGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2+MDcyNDU0WhcNNDMwMzI2MDcyNDU0WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERh+dGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx+GTAXBgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATEKI6rGFtq+vm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7TmFy8as10CW4kjPMIRBSqn+iBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68KjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD+VR0OBBYEFI0GZnQkdjrzife81r1HfS+8EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNo+ADBlAjADVS2m5hjEfO/JUG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0+QoSZ/6vnnvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k=+-----END CERTIFICATE-----++Certum Trusted Root CA+======================+# Fingerprint (SHA-256): FE:76:96:57:38:55:77:3E:37:A9:5E:7A:D4:D9:CC:96:C3:01:57:C1:5D:31:76:5B:A9:B1:57:04:E1:AE:78:FD+-----BEGIN CERTIFICATE-----+MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6MQswCQYDVQQG+EwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0g+Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0Ew+HhcNMTgwMzE2MTIxMDEzWhcNNDMwMzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMY+QXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBB+dXRob3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEB+AQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZn0EGze2jusDbCSzBfN8p+fktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/qp1x4EaTByIVcJdPTsuclzxFUl6s1wB52+HO8AU5853BSlLCIls3Jy/I2z5T4IHhQqNwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2+fJmItdUDmj0VDT06qKhF8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGt+g/BKEiJ3HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGamqi4+NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi7VdNIuJGmj8PkTQk+fVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSFytKAQd8FqKPVhJBPC/PgP5sZ0jeJ+P/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0PqafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSY+njYJdmZm/Bo/6khUHL4wvYBQv3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHK+HRzQ+8S1h9E6Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1+vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQADggIBAEii1QAL+LtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4WxmB82M+w85bj/UvXgF2Ez8s+ALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvozMrnadyHncI013nR03e4qllY/p0m+jiGPp2K+h2RX5Rc64vmNueMzeMGQ2Ljdt4NR5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8+CYyqOhNf6DR5UMEQGfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA+4kZf5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq0Uc9Nneo+WWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7DP78v3DSk+yshzWePS/Tj+6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTMqJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmT+OPQD8rv7gmsHINFSH5pkAnuYZttcTVoP0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZck+bxJF0WddCajJFdr60qZfE2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb+-----END CERTIFICATE-----++TunTrust Root CA+================+# Fingerprint (SHA-256): 2E:44:10:2A:B5:8C:B8:54:19:45:1C:8E:19:D9:AC:F3:66:2C:AF:BC:61:4B:6A:53:96:0A:30:F7:D0:E2:EB:41+-----BEGIN CERTIFICATE-----+MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQELBQAwYTELMAkG+A1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUgQ2VydGlmaWNhdGlvbiBFbGVj+dHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJvb3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQw+NDI2MDg1NzU2WjBhMQswCQYDVQQGEwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBD+ZXJ0aWZpY2F0aW9uIEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIw+DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZn56eY+hz+2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd2JQDoOw05TDENX37Jk0b+bjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgFVwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7+NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZGoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAd+gjH8KcwAWJeRTIAAHDOFli/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViW+VSHbhlnUr8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2eY8f+Tpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIbMlEsPvLfe/ZdeikZ+juXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISgjwBUFfyRbVinljvrS5YnzWuioYas+DXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwS+VXAkPcvCFDVDXSdOvsC9qnyW5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI+04Y+oXNZtPdEITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0+90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+zxiD2BkewhpMl+0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYuQEkHDVneixCwSQXi/5E/S7fd+Ao74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRY+YdZ2vyJ/0Adqp2RT8JeNnYA/u8EH22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJp+adbGNjHh/PqAulxPxOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65x+xBzndFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5Xc0yGYuP+jCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7bnV2UqL1g52KAdoGDDIzM+MEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQCvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9z+ZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZHu/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3r+AZ3r2OvEhJn7wAzMMujjd9qDRIueVSjAi1jTkD5OGwDxFa2DK5o=+-----END CERTIFICATE-----++HARICA TLS RSA Root CA 2021+===========================+# Fingerprint (SHA-256): D9:5D:0E:8E:DA:79:52:5B:F9:BE:B1:1B:14:D2:10:0D:32:94:98:5F:0C:62:D9:FA:BD:9C:D9:99:EC:CB:7B:1D+-----BEGIN CERTIFICATE-----+MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBsMQswCQYDVQQG+EwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9u+cyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0EgUm9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUz+OFoXDTQ1MDIxMzEwNTUzN1owbDELMAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRl+bWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNB+IFJvb3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569lmwVnlskN+JLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE4VGC/6zStGndLuwRo0Xu+a2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uva9of08WRiFukiZLRgeaMOVig1mlDqa2Y+Ulhu2wr7a89o+uOkXjpFc5gH6l8Cct4MpbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K+5FrZx40d/JiZ+yykgmvwKh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEv+dmn8kN3bLW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcYAuUR+0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqBAGMUuTNe3QvboEUH+GjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYqE613TBoYm5EPWNgGVMWX+Ko/IIqm+haZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHrW2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQ+CPxrvrNQKlr9qEgYRtaQQJKQCoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8G+A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE+AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAUX15QvWiWkKQU+EapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3f5Z2EMVGpdAgS1D0NTsY9FVq+QRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxajaH6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxD+QpSbIPDRzbLrLFPCU3hKTwSUQZqPJzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcR+j88YxeMn/ibvBZ3PzzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5+vZStjBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0/L5H9MG0+qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pTBGIBnfHAT+7hOtSLIBD6+Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79aPib8qXPMThcFarmlwDB31qlpzmq6YR/+PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YWxw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnn+kf3/W9b3raYvAwtt41dU63ZTGI0RmLo=+-----END CERTIFICATE-----++HARICA TLS ECC Root CA 2021+===========================+# Fingerprint (SHA-256): 3F:99:CC:47:4A:CF:CE:4D:FE:D5:87:94:66:5E:47:8D:15:47:73:9F:2E:78:0F:1B:B4:CA:9B:13:30:97:D4:01+-----BEGIN CERTIFICATE-----+MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQswCQYDVQQGEwJH+UjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBD+QTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9vdCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoX+DTQ1MDIxMzExMDEwOVowbDELMAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWlj+IGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJv+b3QgQ0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7KKrxcm1l+AEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9YSTHMmE5gEYd103KUkE+b+ECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW+0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAi+rcJRQO9gcS3ujwLEXQNwSaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/Qw+CZ61IygNnxS2PFOiTAZpffpskcYqSUXm7LcT4Tps+-----END CERTIFICATE-----++Autoridad de Certificacion Firmaprofesional CIF A62634068+=========================================================+# Fingerprint (SHA-256): 57:DE:05:83:EF:D2:B2:6E:03:61:DA:99:DA:9D:F4:64:8D:EF:7E:E8:44:1C:3B:72:8A:FA:9B:CD:E0:F9:B2:6A+-----BEGIN CERTIFICATE-----+MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCRVMxQjBA+BgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2+MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIw+QAYDVQQDDDlBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBB+NjI2MzQwNjgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDD+Utd9thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQMcas9UX4P+B99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefGL9ItWY16Ck6WaVICqjaY+7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15iNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqH+ECNZXyAFGUftaI6SEspd/NYrspI8IM/hX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyI+plD9amML9ZMWGxmPsu2bm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctX+MbScyJCyZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirjaEbsX+LZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/TKI8xWVvTyQKmtFLK+bpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF6NkBiDkal4ZkQdU7hwxu+g/GvUgU+vzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVhOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1Ud+DgQWBBRlzeurNR4APn7VdMActHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4w+gZswgZgGBFUdIAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j+b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABCAG8AbgBhAG4A+bwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAwADEANzAOBgNVHQ8BAf8EBAMC+AQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9miWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL+4QjbEwj4KKE1soCzC1HA01aajTNFSa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDb+LIpgD7dvlAceHabJhfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1il+I45PVf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZEEAEeiGaP+cjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV1aUsIC+nmCjuRfzxuIgA+LI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2tCsvMo2ebKHTEm9caPARYpoKdrcd7b/+A+lun4jWq9GJAd/0kakFI3ky88Al2CdgtR5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH+9IBk9W6VULgRfhVwOEqwf9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpf+NIbnYrX9ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNKGbqE+ZycPvEJdvSRUDewdcAZfpLz6IHxV+-----END CERTIFICATE-----++vTrus ECC Root CA+=================+# Fingerprint (SHA-256): 30:FB:BA:2C:32:23:8E:2A:98:54:7A:F9:79:31:E5:50:42:8B:9B:3F:1C:8E:EB:66:33:DC:FA:86:C5:B2:7D:D3+-----BEGIN CERTIFICATE-----+MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMwRzELMAkGA1UE+BhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBS+b290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDczMTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAa+BgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYw+EAYHKoZIzj0CAQYFK4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+c+ToL0v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUde4BdS49n+TPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYDVR0TAQH/BAUwAwEB/zAO+BgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIwV53dVvHH4+m4SVBrm2nDb+zDfSXkV5UT+QJtS0zvzQBm8JsctBp61ezaf9SXUY2sAAjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQL+YgmRWAD5Tfs0aNoJrSEGGJTO+-----END CERTIFICATE-----++vTrus Root CA+=============+# Fingerprint (SHA-256): 8A:71:DE:65:59:33:6F:42:6C:26:E5:38:80:D0:0D:88:A1:8D:A4:C6:A9:1F:0D:CB:61:94:E2:06:C5:C9:63:87+-----BEGIN CERTIFICATE-----+MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQELBQAwQzELMAkG+A1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xFjAUBgNVBAMTDXZUcnVzIFJv+b3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMxMDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoG+A1UEChMTaVRydXNDaGluYSBDby4sTHRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJ+KoZIhvcNAQEBBQADggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZots+SKYcIrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykUAyyNJJrI+ZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+GrPSbcKvdmaVayqwlHeF+XgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z98Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KA+YPxMvDVTAWqXcoKv8R1w6Jz1717CbMdHflqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70+kLJrxLT5ZOrpGgrIDajtJ8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2+AXPKBlim0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZNpGvu+/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQUqqzApVg+QxMaPnu+1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHWOXSuTEGC2/KmSNGzm/MzqvOmwMVO+9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMBAAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYg+scasGrz2iTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOC+AgEAKbqSSaet8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd+nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1jbhd47F18iMjr+jld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvMKar5CKXiNxTKsbhm7xqC5PD4+8acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIivTDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJn+xDHO2zTlJQNgJXtxmOTAGytfdELSS8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554Wg+icEFOwE30z9J4nfrI8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4+sEb9b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNBUvupLnKW+nyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1PTi07NEPhmg4NpGaXutIc+SkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929vensBxXVsFy6K2ir40zSbofitzmdHxghm+H+l3s=+-----END CERTIFICATE-----++ISRG Root X2+============+# Fingerprint (SHA-256): 69:72:9B:8E:15:A8:6E:FC:17:7A:57:AF:B7:17:1D:FC:64:AD:D2:8C:2F:CA:8C:F1:50:7E:34:45:3C:CB:14:70+-----BEGIN CERTIFICATE-----+MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQswCQYDVQQGEwJV+UzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElT+UkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVT+MSkwJwYDVQQKEyBJbnRlcm5ldCBTZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNS+RyBSb290IFgyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0H+ttwW+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9ItgKbppb+d9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV+HQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZIzj0EAwMDaAAwZQIwe3lORlCEwkSHRhtF+cP9Ymd70/aTSVaYgLXTWNLxBo1BfASdWtL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5+U6VR5CmD1/iQMVtCnwr1/q4AaOeMSQ+2b1tbFfLn+-----END CERTIFICATE-----++HiPKI Root CA - G1+==================+# Fingerprint (SHA-256): F0:15:CE:3C:C2:39:BF:EF:06:4B:E9:F1:D2:C4:17:E1:A0:26:4A:0A:94:BE:1F:0C:8D:12:18:64:EB:69:49:CC+-----BEGIN CERTIFICATE-----+MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBPMQswCQYDVQQG+EwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0ZC4xGzAZBgNVBAMMEkhpUEtJ+IFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRaFw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYT+AlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kg+Um9vdCBDQSAtIEcxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0+o9QwqNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twvVcg3Px+k+wJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6lZgRZq2XNdZ1AYDgr/SE+YYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnzQs7ZngyzsHeXZJzA9KMuH5UHsBffMNsA+GJZMoYFL3QRtU6M9/Aes1MU3guvklQgZKILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfd+hSi8MEyr48KxRURHH+CKFgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj+1jOXTyFjHluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDry+K4+9a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ/W3c1pzAtH2lsN0/+Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgMa/aOEmem8rJY5AIJEzypuxC00jBF+8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYD+VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQD+AgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi+7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqcSE5XCV0vrPSl+tJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6FzaZsT0pPBWGTMpWmWSBUdGSquE+wx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9TcXzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07Q+JNBAsNB1CI69aO4I1258EHBGG3zgiLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv+5wiZqAxeJoBF1PhoL5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+Gpz+jLrFNe85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wrkkVbbiVg+hUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+vhV4nYWBSipX3tUZQ9rb+yltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQUYDksswBVLuT1sw5XxJFBAJw/6KXf6vb/+yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ==+-----END CERTIFICATE-----++GlobalSign ECC Root CA - R4+===========================+# Fingerprint (SHA-256): B0:85:D7:0B:96:4F:19:1A:73:E4:AF:0D:54:AE:7A:0E:07:AA:FD:AF:9B:71:DD:08:62:13:8A:B7:32:5A:24:A2+-----BEGIN CERTIFICATE-----+MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYDVQQLExtHbG9i+YWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkds+b2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgwMTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9i+YWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkds+b2JhbFNpZ24wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkW+ymOxuYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNVHQ8BAf8E+BAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/+wpu+74zyTyjhNUwCgYI+KoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147bmF0774BxL4YSFlhgjICICadVGNA3jdg+UM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm+-----END CERTIFICATE-----++GTS Root R1+===========+# Fingerprint (SHA-256): D9:47:43:2A:BD:E7:B7:FA:90:FC:2E:6B:59:10:1B:12:80:E0:E1:C7:E4:E4:0F:A3:C6:88:7F:FF:57:A7:F4:CF+-----BEGIN CERTIFICATE-----+MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQGEwJV+UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg+UjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE+ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0G+CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaM+f/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7wCl7raKb0+xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjwTcLCeoiKu7rPWRnWr4+w+B7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0PfyblqAj+lug8aJRT7oM6iCsVlgmy4HqMLnXW+nOunVmSPlk9orj2XwoSPwLxAwAtcvfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk+9+aCEI3oncKKiPo4Zor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zq+kUspzBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92wO1A+K/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70paDPvOmbsB4om3xPX+V2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrNVjzRlwW5y0vtOUucxD/SVRNuJLDW+cfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0T+AQH/BAUwAwEB/zAdBgNVHQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQAD+ggIBAJ+qQibbC5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe+QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuyh6f88/qBVRRi+ClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM47HLwEXWdyzRSjeZ2axfG34ar+J45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8JZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYci+NuaCp+0KueIHoI17eko8cdLiA6EfMgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5me+LMFrUKTX5hgUvYU/Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJF+fbdT6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ0E6yove++7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm2tIMPNuzjsmhDYAPexZ3+FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bbbP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3+gm3c+-----END CERTIFICATE-----++GTS Root R3+===========+# Fingerprint (SHA-256): 34:D8:A7:3E:E2:08:D9:BC:DB:0D:95:65:20:93:4B:4E:40:E6:94:82:59:6E:8B:6F:73:C8:42:6B:01:0A:6F:48+-----BEGIN CERTIFICATE-----+MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJVUzEi+MCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMw+HhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZ+R29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjO+PQIBBgUrgQQAIgNiAAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout+736GjOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL24CejQjBA+MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTB8Sa6oC2uhYHP0/Eq+Er24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azT+L818+FsuVbu/3ZL3pAzcMeGiAjEA/JdmZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV+11RZt+cRLInUue4X+-----END CERTIFICATE-----++GTS Root R4+===========+# Fingerprint (SHA-256): 34:9D:FA:40:58:C5:E2:63:12:3B:39:8A:E7:95:57:3C:4E:13:13:C8:3F:E6:8F:93:55:6C:D5:E8:03:1B:3C:7D+-----BEGIN CERTIFICATE-----+MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJVUzEi+MCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQw+HhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZ+R29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjO+PQIBBgUrgQQAIgNiAATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzu+hXyiQHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvRHYqjQjBA+MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSATNbrdP9JNqPV2Py1+PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/C+r8deVl5c1RxYIigL9zC2L7F8AjEA8GE8p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh+4rsUecrNIdSUtUlD+-----END CERTIFICATE-----++Telia Root CA v2+================+# Fingerprint (SHA-256): 24:2B:69:74:2F:CB:1E:5B:2A:BF:98:89:8B:94:57:21:87:54:4E:5B:4D:99:11:78:65:73:62:1F:6A:74:B8:2C+-----BEGIN CERTIFICATE-----+MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQxCzAJBgNVBAYT+AkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2+MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQK+DBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZI+hvcNAQEBBQADggIPADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ7+6zBqAMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9vVYiQJ3q+9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9lRdU2HhE8Qx3FZLgmEKn+pNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTODn3WhUidhOPFZPY5Q4L15POdslv5e2QJl+tI5c0BE0312/UqeBAMN/mUWZFdUXyApT7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW+5olWK8jjfN7j/4nlNW4o6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNr+RBH0pUPCTEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6WT0E+BXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63RDolUK5X6wK0dmBR4+M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZIpEYslOqodmJHixBTB0hXbOKSTbau+BcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGjYzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7W+xy+G2CQ5MB0GA1UdDgQWBBRyrOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYD+VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ+8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi0f6X+J8wfBj5+tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMMA8iZGok1GTzTyVR8qPAs5m4H+eW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBSSRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+C+y748fdHif64W1lZYudogsYMVoe+KTTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygC+QMez2P2ccGrGKMOF6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15+h2Er3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMtTy3EHD70+sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pTVmBds9hCG1xLEooc6+t9+xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAWysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQ+raVplI/owd8k+BsHMYeB2F326CjYSlKArBPuUBQemMc=+-----END CERTIFICATE-----++D-TRUST BR Root CA 1 2020+=========================+# Fingerprint (SHA-256): E5:9A:AA:81:60:09:C2:2B:FF:5B:25:BA:D3:7D:F3:06:F0:49:79:7C:1F:81:D8:5A:B0:89:E6:57:BD:8F:00:44+-----BEGIN CERTIFICATE-----+MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQswCQYDVQQGEwJE+RTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRSVVNUIEJSIFJvb3QgQ0EgMSAy+MDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNV+BAoTDEQtVHJ1c3QgR21iSDEiMCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAG+ByqGSM49AgEGBSuBBAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7+dPYSzuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0QVK5buXu+QqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/VbNafAkl1bK6CKBrqx9t+MA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6gPKA6hjhodHRwOi8vY3JsLmQtdHJ1c3Qu+bmV0L2NybC9kLXRydXN0X2JyX3Jvb3RfY2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVj+dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxP+PUQtVHJ1c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjOPQQD+AwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFWwKrY7RjEsK70Pvom+AjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHVdWNbFJWcHwHP2NVypw87+-----END CERTIFICATE-----++D-TRUST EV Root CA 1 2020+=========================+# Fingerprint (SHA-256): 08:17:0D:1A:A3:64:53:90:1A:2F:95:92:45:E3:47:DB:0C:8D:37:AB:AA:BC:56:B8:1A:A1:00:DC:95:89:70:DB+-----BEGIN CERTIFICATE-----+MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQswCQYDVQQGEwJE+RTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRSVVNUIEVWIFJvb3QgQ0EgMSAy+MDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNV+BAoTDEQtVHJ1c3QgR21iSDEiMCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAG+ByqGSM49AgEGBSuBBAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8+ZRCC/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rDwpdhQntJ+raOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3OqQo5FD4pPfsazK2/umL+MA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6gPKA6hjhodHRwOi8vY3JsLmQtdHJ1c3Qu+bmV0L2NybC9kLXRydXN0X2V2X3Jvb3RfY2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVj+dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxP+PUQtVHJ1c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjOPQQD+AwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CAy/m0sRtW9XLS/BnR+AjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJbgfM0agPnIjhQW+0ZT0MW+-----END CERTIFICATE-----++DigiCert TLS ECC P384 Root G5+=============================+# Fingerprint (SHA-256): 01:8E:13:F0:77:25:32:CF:80:9B:D1:B1:72:81:86:72:83:FC:48:C6:E1:3B:E9:C6:98:12:85:4A:49:0C:1B:05+-----BEGIN CERTIFICATE-----+MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQswCQYDVQQGEwJV+UzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURpZ2lDZXJ0IFRMUyBFQ0MgUDM4+NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMx+FzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQg+Um9vdCBHNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1Tzvd+lHJS7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp0zVozptj+n4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICISB4CIfBFqMA4GA1UdDwEB+/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQCJao1H5+z8blUD2Wds+Jk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQLgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIx+AJSdYsiJvRmEFOml+wG4DXZDjC5Ty3zfDBeWUA==+-----END CERTIFICATE-----++DigiCert TLS RSA4096 Root G5+============================+# Fingerprint (SHA-256): 37:1A:00:DC:05:33:B3:72:1A:7E:EB:40:E8:41:9E:70:79:9D:2B:0A:0F:2C:1D:80:69:31:65:F7:CE:C4:AD:75+-----BEGIN CERTIFICATE-----+MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBNMQswCQYDVQQG+EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0+MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcNNDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJV+UzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2+IFJvb3QgRzUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS8+7IE+ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG02C+JFvuU+AT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgpwgscONyfMXdcvyej/Ces+tyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZMpG2T6T867jp8nVid9E6P/DsjyG244gXa+zOvswzH016cpVIDPRFtMbzCe88zdH5RDnU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnV+DdXifBBiqmvwPXbzP6PosMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9q+TXeXAaDxZre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cdLvvy+z6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvXKyY//SovcfXWJL5/+MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNeXoVPzthwiHvOAbWWl9fNff2C+MIk+wcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPLtgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4E+FgQUUTMc7TZArxfTJc1paPKvTiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8w+DQYJKoZIhvcNAQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw+GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7HPNtQOa27PShN+lnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLFO4uJ+DQtpBflF+aZfTCIITfN+MBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQREtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/+u4cnYiWB39yhL/btp/96j1EuMPikAdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9G+OUrYU9DzLjtxpdRv/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh+47a+p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilwMUc/dNAU+FvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WFqUITVuwhd4GTWgzqltlJ+yqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCKovfepEWFJqgejF0pW8hL2JpqA15w8oVP+bEtoL8pU9ozaMv7Da4M/OMZ++-----END CERTIFICATE-----++Certainly Root R1+=================+# Fingerprint (SHA-256): 77:B8:2C:D8:64:4C:43:05:F7:AC:C5:CB:15:6B:45:67:50:04:03:3D:51:C6:0C:62:02:A8:E0:C3:34:67:D3:A0+-----BEGIN CERTIFICATE-----+MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAwPTELMAkGA1UE+BhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2VydGFpbmx5IFJvb3QgUjEwHhcN+MjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2Vy+dGFpbmx5MRowGAYDVQQDExFDZXJ0YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIP+ADCCAgoCggIBANA21B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O+5MQTvqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbedaFySpvXl+8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b01C7jcvk2xusVtyWMOvwl+DbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGI+XsXwClTNSaa/ApzSRKft43jvRl5tcdF5cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkN+KPl6I7ENPT2a/Z2B7yyQwHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQ+AjeZjOVJ6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA2Cnb+rlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyHWyf5QBGenDPBt+U1+VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMReiFPCyEQtkA6qyI6BJyLm4SGcprS+p6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud+DgQWBBTgqj8ljZ9EXME66C6ud0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAsz+HQNTVfSVcOQrPbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d+8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi1wrykXprOQ4v+MMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrdrRT90+7iIgXr0PK3aBLXWopB+GsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9ditaY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y++gjwN/KUD+nsa2UUeYNrEjvn8K8l7lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgH+JBu6haEaBQmAupVjyTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7+fpYnKx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLyyCwzk5Iw+x06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5nwXARPbv0+Em34yaXOp/S+X3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6OV+KmalBWQewLK8=+-----END CERTIFICATE-----++Certainly Root E1+=================+# Fingerprint (SHA-256): B4:58:5F:22:E4:AC:75:6A:4E:86:12:A1:36:1C:5D:9D:03:1A:93:FD:84:FE:BB:77:8F:A3:06:8B:0F:C4:2D:C2+-----BEGIN CERTIFICATE-----+MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQswCQYDVQQGEwJV+UzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlubHkgUm9vdCBFMTAeFw0yMTA0+MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJBgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlu+bHkxGjAYBgNVBAMTEUNlcnRhaW5seSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4+fxzf7flHh4axpMCK+IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9+YBk2QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8EBAMCAQYw+DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4hevIIgcwCgYIKoZIzj0E+AwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozmut6Dacpps6kFtZaSF4fC0urQe87YQVt8+rgIwRt7qy12a7DLCZRawTDBcMPPaTnOGBtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR+-----END CERTIFICATE-----++Security Communication ECC RootCA1+==================================+# Fingerprint (SHA-256): E7:4F:BD:A5:5B:D5:64:C4:73:A3:6B:44:1A:A7:99:C8:A6:8E:07:74:40:E8:28:8B:9F:A1:E5:0E:4B:BA:CA:11+-----BEGIN CERTIFICATE-----+MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYTAkpQMSUwIwYD+VQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYDVQQDEyJTZWN1cml0eSBDb21t+dW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYxNjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTEL+MAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNV+BAMTIlNlY3VyaXR5IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQA+IgNiAASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+CnnfdldB9sELLo+5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpKULGjQjBAMB0GA1UdDgQW+BBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAK+BggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3L+snNdo4gIxwwCMQDAqy0Obe0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70e+N9k=+-----END CERTIFICATE-----++BJCA Global Root CA1+====================+# Fingerprint (SHA-256): F3:89:6F:88:FE:7C:0A:88:27:66:A7:FA:6A:D2:74:9F:B5:7A:7F:3E:98:FB:76:9C:1F:A7:B0:9C:2C:44:D5:AE+-----BEGIN CERTIFICATE-----+MIIFdDCCA1ygAwIBAgIQVW9l47TZkGobCdFsPsBsIDANBgkqhkiG9w0BAQsFADBUMQswCQYDVQQG+EwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJVFkxHTAbBgNVBAMMFEJK+Q0EgR2xvYmFsIFJvb3QgQ0ExMB4XDTE5MTIxOTAzMTYxN1oXDTQ0MTIxMjAzMTYxN1owVDELMAkG+A1UEBhMCQ04xJjAkBgNVBAoMHUJFSUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQD+DBRCSkNBIEdsb2JhbCBSb290IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPFm+CL3ZxRVhy4QEQaVpN3cdwbB7+sN3SJATcmTRuHyQNZ0YeYjjlwE8R4HyDqKYDZ4/N+AZspDyRhyS+sTphzvq3Rp4Dhtczbu33RYx2N95ulpH3134rhxfVizXuhJFyV9xgw8O558dnJCNPYwpj9mZ9S1Wn+P3hkSWkSl+BMDdMJoDIwOvqfwPKcxRIqLhy1BDPapDgRat7GGPZHOiJBhyL8xIkoVNiMpTAK+BcW+yqw3/XmnkRd4OJmtWO2y3syJfQOcs4ll5+M7sSKGjwZteAf9kRJ/sGsciQ35uMt0WwfCyPQ10WRj+eulumijWML3mG90Vr4TqnMfK9Q7q8l0ph49pczm+LiRvRSGsxdRpJQaDrXpIhRMsDQa4bHlW/KNn+MoH1V6XKV0Jp6VwkYe/iMBhORJhVb3rCk9gZtt58R4oRTklH2yiUAguUSiz5EtBP6DF+bHq/pj+b+OT0CFqMYs2esWz8sgytnOYFcuX6U1WTdno9uruh8W7TXakdI136z1C2OVnZOz2nxbkRs1CTqjSSh+GL+9V/6pmTW12xB3uD1IutbB5/EjPtffhZ0nPNRAvQoMvfXnjSXWgXSHRtQpdaJCbPdzied9v3pK+H9MiyRVVz99vfFXQpIsHETdfg6YmV6YBW37+WGgHqel62bno/1Afq8K0wM7o6v0PvY1NuLxxAgMB+AAGjQjBAMB0GA1UdDgQWBBTF7+3M2I0hxkjk49cULqcWk+WYATAPBgNVHRMBAf8EBTADAQH/MA4G+A1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAUoKsITQfI/Ki2Pm4rzc2IInRNwPWaZ+4+YRC6ojGYWUfo0Q0lHhVBDOAqVdVXUsv45Mdpox1NcQJeXyFFYEhcCY5JEMEE3KliawLwQ8hOnThJ+dMkycFRtwUf8jrQ2ntScvd0g1lPJGKm1Vrl2i5VnZu69mP6u775u+2D2/VnGKhs/I0qUJDAnyIm8+60Qkmss9vk/Ves6OF8tiwdneHg56/0OGNFK8YT88X7vZdrRTvJez/opMEi4r89fO4aL/3Xtw+zuh+TaRjAv04l5U/BXCga99igUOLtFkNSoxUnMW7gZ/NfaXvCyUeOiDbHPwfmGcCCtRzRBPbUYQaVQNW+4AB+dAb/OMRyHdOoP2gxXdMJxy6MW2Pg6Nwe0uxhHvLe5e/2mXZgLR6UcnHGCyoyx5JO1UbXHfmp+GQrI+pXObSOYqgs4rZpWDW+N8TEAiMEXnM0ZNjX+VVOg4DwzX5Ze4jLp3zO7Bkqp2IRzznfSxqxx+4VyjHQy7Ct9f4qNx2No3WqB4K/TUfet27fJhcKVlmtOJNBir+3I+17Q9eVzYH6Eze9mCUAyTF6ps+3MKCuwJXNq+YJyo5UOGwifUll35HaBC07HPKs5fRJNz2YqAo07WjuGS3iGJCz51TzZm+ZGiPTx4S+SPfSKcOYKMryMguTjClPPGAyzQWWYezyr/6zcCwupvI=+-----END CERTIFICATE-----++BJCA Global Root CA2+====================+# Fingerprint (SHA-256): 57:4D:F6:93:1E:27:80:39:66:7B:72:0A:FD:C1:60:0F:C2:7E:B6:6D:D3:09:29:79:FB:73:85:64:87:21:28:82+-----BEGIN CERTIFICATE-----+MIICJTCCAaugAwIBAgIQLBcIfWQqwP6FGFkGz7RK6zAKBggqhkjOPQQDAzBUMQswCQYDVQQGEwJD+TjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJVFkxHTAbBgNVBAMMFEJKQ0Eg+R2xvYmFsIFJvb3QgQ0EyMB4XDTE5MTIxOTAzMTgyMVoXDTQ0MTIxMjAzMTgyMVowVDELMAkGA1UE+BhMCQ04xJjAkBgNVBAoMHUJFSUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRC+SkNBIEdsb2JhbCBSb290IENBMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABJ3LgJGNU2e1uVCxA/jl+SR9BIgmwUVJY1is0j8USRhTFiy8shP8sbqjV8QnjAyEUxEM9fMEsxEtqSs3ph+B99iK++kpRuDCK+/eHeGBIK9ke35xe/J4rUQUyWPGCWwf0VHKNCMEAwHQYDVR0OBBYEFNJKsVF/BvDRgh9Obl+rg/xI+1LCRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMBq8+W9f+qdJUDkpd0m2xQNz0Q9XSSpkZElaA94M04TVOSG0ED1cxMDAtsaqdAzjbBgIxAMvMh1PLet8g+UXOQwKhbYdDFUDn9hf7B43j4ptZLvZuHjw/l1lOWqzzIQNph91Oj9w==+-----END CERTIFICATE-----++Sectigo Public Server Authentication Root E46+=============================================+# Fingerprint (SHA-256): C9:0F:26:F0:FB:1B:40:18:B2:22:27:51:9B:5C:A2:B5:3E:2C:A5:B3:BE:5C:F1:8E:FE:1B:EF:47:38:0C:53:83+-----BEGIN CERTIFICATE-----+MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQswCQYDVQQGEwJH+QjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBTZXJ2+ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5+WjBfMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0+aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUr+gQQAIgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccCWvkEN/U0+NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+6xnOQ6OjQjBAMB0GA1Ud+DgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB+/zAKBggqhkjOPQQDAwNnADBkAjAn7qRaqCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RH+lAFWovgzJQxC36oCMB3q4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21U+SAGKcw==+-----END CERTIFICATE-----++Sectigo Public Server Authentication Root R46+=============================================+# Fingerprint (SHA-256): 7B:B6:47:A6:2A:EE:AC:88:BF:25:7A:A5:22:D0:1F:FE:A3:95:E0:AB:45:C7:3F:93:F6:56:54:EC:38:F2:5A:06+-----BEGIN CERTIFICATE-----+MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBfMQswCQYDVQQG+EwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT+ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwHhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1+OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T+ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3+DQEBAQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDaef0rty2k+1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnzSDBh+oF8HqcIStw+Kxwf+GExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xfiOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMP+FF1bFOdLvt30yNoDN9HWOaEhUTCDsG3XME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vu+ZDCQOc2TZYEhMbUjUDM3IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5Qaz+Yw6A3OASVYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgESJ/A+wSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu+Zd4KKTIRJLpfSYF+plhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt8uaZFURww3y8nDnAtOFr94MlI1fZ+EoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+LHaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW+6aWWrL3DkJiy4Pmi1KZHQ3xtzwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWI+IUkwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c+mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQYKlJfp/imTYp+E0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52gDY9hAaLMyZlbcp+nv4fjFg4+exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZAFv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M+0ejf5lG5Nkc/kLnHvALcWxxPDkjBJYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI+84HxZmduTILA7rpXDhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9m+pFuiTdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5dHn5Hrwd+Vw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65LvKRRFHQV80MNNVIIb/b+E/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmm+J1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAYQqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL+-----END CERTIFICATE-----++SSL.com TLS RSA Root CA 2022+============================+# Fingerprint (SHA-256): 8F:AF:7D:2E:2C:B4:70:9B:B8:E0:B3:36:66:BF:75:A5:DD:45:B5:DE:48:0F:8E:A8:D4:BF:E6:BE:BC:17:F2:ED+-----BEGIN CERTIFICATE-----+MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQG+EwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxTU0wuY29tIFRMUyBSU0Eg+Um9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloXDTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMC+VVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJv+b3QgQ0EgMjAyMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u+9nTPL3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OYt6/wNr/y+7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0insS657Lb85/bRi3pZ7Qcac+oOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3PnxEX4MN8/HdIGkWCVDi1FW24IBydm5M+R7d1VVm0U3TZlMZBrViKMWYPHqIbKUBOL9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDG+D6C1vBdOSHtRwvzpXGk3R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEW+TO6Af77wdr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS+YCk+8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYSd66UNHsef8JmAOSq+g+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoGAtUjHBPW6dvbxrB6y3snm/vg1UYk+7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2fgTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1Ud+EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsu+N+7jhHonLs0ZNbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt+hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsMQtfhWsSWTVTN+j8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvfR4iyrT7gJ4eLSYwfqUdYe5by+iB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJDPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjU+o3KUQyxi4U5cMj29TH0ZR6LDSeeWP4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqo+ENjwuSfr98t67wVylrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7Egkaib+MOlqbLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2wAgDHbICi+vRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3qr5nsLFR+jM4uElZI7xc7+P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sjiMho6/4UIyYOf8kpIEFR3N+2ivEC+5BB0+9+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA=+-----END CERTIFICATE-----++SSL.com TLS ECC Root CA 2022+============================+# Fingerprint (SHA-256): C3:2F:FD:9F:46:F9:36:D1:6C:36:73:99:09:59:43:4B:9A:D6:0A:AF:BB:9E:7C:F3:36:54:F1:44:CC:1B:A1:43+-----BEGIN CERTIFICATE-----+MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQswCQYDVQQGEwJV+UzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxTU0wuY29tIFRMUyBFQ0MgUm9v+dCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMx+GDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3Qg+Q0EgMjAyMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWy+JGYmacCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFNSeR7T5v1+5wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSJjy+j6CugFFR7+81a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NWuCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGG+MAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w+7deedWo1dlJF4AIxAMeNb0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5+Zn6g6g==+-----END CERTIFICATE-----++Atos TrustedRoot Root CA ECC TLS 2021+=====================================+# Fingerprint (SHA-256): B2:FA:E5:3E:14:CC:D7:AB:92:12:06:47:01:AE:27:9C:1D:89:88:FA:CB:77:5F:A8:A0:08:91:4E:66:39:88:A8+-----BEGIN CERTIFICATE-----+MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4wLAYDVQQDDCVB+dG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0wCwYDVQQKDARBdG9zMQswCQYD+VQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3Mg+VHJ1c3RlZFJvb3QgUm9vdCBDQSBFQ0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYT+AkRFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6K+DP/XtXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4AjJn8ZQS+b+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2KCXWfeBmmnoJsmo7jjPX+NtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMDaAAwZQIwW5kp85wxtolrbNa9d+F851F++uDrNozZffPc8dz7kUK2o59JZDCaOMDtuCCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGY+a3cpetskz2VAv9LcjBHo9H1/IISpQuQo+-----END CERTIFICATE-----++Atos TrustedRoot Root CA RSA TLS 2021+=====================================+# Fingerprint (SHA-256): 81:A9:08:8E:A5:9F:B3:64:C5:48:A6:F8:55:59:09:9B:6F:04:05:EF:BF:18:E5:32:4E:C9:F4:57:BA:00:11:2F+-----BEGIN CERTIFICATE-----+MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBMMS4wLAYDVQQD+DCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIxMQ0wCwYDVQQKDARBdG9zMQsw+CQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0+b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNV+BAYTAkRFMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BB+l01Z4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYvYe+W/CBG+vevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZkmGbzSoXfduP9LVq6hdK+ZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDsGY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt+0xU6kGpn8bRrZtkh68rZYnxGEFzedUlnnkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVK+PNe0OwANwI8f4UDErmwh3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMY+sluMWuPD0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzygeBY+Br3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8ANSbhqRAvNncTFd++rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezBc6eUWsuSZIKmAMFwoW4sKeFYV+xa+fJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lIpw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/+BAUwAwEB/zAdBgNVHQ4EFgQUdEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0G+CSqGSIb3DQEBDAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS+4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPso0UvFJ/1TCpl+Q3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJqM7F78PRreBrAwA0JrRUITWX+AdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuywxfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9G+slA9hGCZcbUztVdF5kJHdWoOsAgMrr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2Vkt+afcxBPTy+av5EzH4AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9q+TFsR0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuYo7Ey7Nmj+1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5dDTedk+SKlOxJTnbPP/l+PqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcEoji2jbDwN/zIIX8/syQbPYtuzE2wFg2W+HYMfRsCbvUOZ58SWLs5fyQ==+-----END CERTIFICATE-----++TrustAsia Global Root CA G3+===========================+# Fingerprint (SHA-256): E0:D3:22:6A:EB:11:63:C2:E4:8F:F9:BE:3B:50:B4:C6:43:1B:E7:BB:1E:AC:C5:C3:6B:5D:5E:C5:09:03:9A:08+-----BEGIN CERTIFICATE-----+MIIFpTCCA42gAwIBAgIUZPYOZXdhaqs7tOqFhLuxibhxkw8wDQYJKoZIhvcNAQEMBQAwWjELMAkG+A1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMsIEluYy4xJDAiBgNVBAMM+G1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHMzAeFw0yMTA1MjAwMjEwMTlaFw00NjA1MTkwMjEw+MTlaMFoxCzAJBgNVBAYTAkNOMSUwIwYDVQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMu+MSQwIgYDVQQDDBtUcnVzdEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzMwggIiMA0GCSqGSIb3DQEBAQUA+A4ICDwAwggIKAoICAQDAMYJhkuSUGwoqZdC+BqmHO1ES6nBBruL7dOoKjbmzTNyPtxNST1QY4Sxz+lZHFZjtqz6xjbYdT8PfxObegQ2OwxANdV6nnRM7EoYNl9lA+sX4WuDqKAtCWHwDNBSHvBm3dIZwZ+Q0WhxeiAysKtQGIXBsaqvPPW5vxQfmZCHzyLpnl5hkA1nyDvP+uLRx+PjsXUjrYsyUQE49RDdT/V+P68czH5GX6zfZBCK70bwkPAPLfSIC7Epqq+FqklYqL9joDiR5rPmd2jE+SoZhLsO4fWvieylL1Ag+dB4SQXMeJNnKziyhWTXAyB1GJ2Faj/lN03J5Zh6fFZAhLf3ti1ZwA0pJPn9pMRJpxx5cynoTi+jm+9WAPzJMshH/x/Gr8m0ed262IPfN2dTPXS6TIi/n1Q1hPy8gDVI+lhXgEGvNz8teHHUGf59gXzhqc+D0r83ERoVGjiQTz+LISGNzzNPy+i2+f3VANfWdP3kXjHi3dqFuVJhZBFcnAvkV34PmVACxmZySYg+WmjBNb9Pp1Hx2BErW+Canig7CjoKH8GB5S7wprlppYiU5msTf9FkPz2ccEblooV7WIQn3MSAPmea+mseaMQ4w7OYXQJXZRe0Blqq/DPNL0WP3E1jAuPP6Z92bfW1K/zJMtSU7/xxnD4UiWQWRkUF3gdCF+TIcQcf+eQxuulXUtgQIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEDk5PIj+7zjKsK5Xf/IhMBY027ySMB0GA1UdDgQWBBRA5OTyI+84yrCuV3/yITAWNNu8kjAOBgNVHQ8BAf8E+BAMCAQYwDQYJKoZIhvcNAQEMBQADggIBACY7UeFNOPMyGLS0XuFlXsSUT9SnYaP4wM8zAQLpw6o1+D/GUE3d3NZ4tVlFEbuHGLige/9rsR82XRBf34EzC4Xx8MnpmyFq2XFNFV1pF1AWZLy4jVe5jaN/T+G3inEpQGAHUNcoTpLrxaatXeL1nHo+zSh2bbt1S1JKv0Q3jbSwTEb93mPmY+KfJLaHEih6D4sTNj+duMNhXJEIlU/HHzp/LgV6FL6qj6jITk1dImmasI5+njPtqzn59ZW/yOSLlALqbUHM/Q4X6RJpstl+cHboCoWASzY9M/eVVHUl2qzEc4Jl6VL1XP04lQJqaTDFHApXB64ipCz5xUG3uOyfT0gA+QEEVcys++TIxxHWVBqB/0Y0n3bOppHKH/lmLmnp0Ft0WpWIp6zqW3IunaFnT63eROfjXy9mPX1onAX1daBli+2MjN9LdyR75bl87yraKZk62Uy5P2EgmVtqvXO9A/EcswFi55gORngS1d7XB4tmBZrOFdRWOPyN9y+aFvqHbgB8X7754qz41SgOAngPN5C8sLtLpvzHzW2NtjjgKGLzZlkD8Kqq7HK9W+eQ42EVJmzbsAS+ZthwEPEGNTNDqJwuuhQxzhB/HIbjj9LV+Hfsm6vxL2PZQl/gZ4FkkfGXL/xuJvYz+NO1+MRiqzFR+JQJ6+N1rZdVtTTDIZbpoFGWsJwt0ivKH+-----END CERTIFICATE-----++TrustAsia Global Root CA G4+===========================+# Fingerprint (SHA-256): BE:4B:56:CB:50:56:C0:13:6A:52:6D:F4:44:50:8D:AA:36:A0:B5:4F:42:E4:AC:38:F7:2A:F4:70:E4:79:65:4C+-----BEGIN CERTIFICATE-----+MIICVTCCAdygAwIBAgIUTyNkuI6XY57GU4HBdk7LKnQV1tcwCgYIKoZIzj0EAwMwWjELMAkGA1UE+BhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMsIEluYy4xJDAiBgNVBAMMG1Ry+dXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHNDAeFw0yMTA1MjAwMjEwMjJaFw00NjA1MTkwMjEwMjJa+MFoxCzAJBgNVBAYTAkNOMSUwIwYDVQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQw+IgYDVQQDDBtUcnVzdEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi+AATxs8045CVD5d4ZCbuBeaIVXxVjAd7Cq92zphtnS4CDr5nLrBfbK5bKfFJV4hrhPVbwLxYI+hW8+m7tH5j/uqOFMjPXTNvk4XatwmkcN4oFBButJ+bAp3TPsUKV/eSm4IJijYzBhMA8GA1UdEwEB/wQF+MAMBAf8wHwYDVR0jBBgwFoAUpbtKl86zK3+kMd6Xg1mDpm9xy94wHQYDVR0OBBYEFKW7SpfOsyt/+pDHel4NZg6ZvccveMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjBe8usGzEkxn0AA+bbd+NvBNEU/zy4k6LHiRUKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xk+dUfFVZDj/bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA==+-----END CERTIFICATE-----++Telekom Security TLS ECC Root 2020+==================================+# Fingerprint (SHA-256): 57:8A:F4:DE:D0:85:3F:4E:59:98:DB:4A:EA:F9:CB:EA:8D:94:5F:60:B6:20:A3:8D:1A:3C:13:B2:BC:7B:A8:E1+-----BEGIN CERTIFICATE-----+MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQswCQYDVQQGEwJE+RTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBHbWJIMSswKQYDVQQDDCJUZWxl+a29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIwMB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIz+NTk1OVowYzELMAkGA1UEBhMCREUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkg+R21iSDErMCkGA1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqG+SM49AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/OtdKPD/M1+2kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDPf8iAC8GXs7s1J8nCG6NC+MEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6fMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P+AQH/BAQDAgEGMAoGCCqGSM49BAMDA2cAMGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZ+Mo7k+5Dck2TOrbRBR2Diz6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdU+ga/sf+Rn27iQ7t0l+-----END CERTIFICATE-----++Telekom Security TLS RSA Root 2023+==================================+# Fingerprint (SHA-256): EF:C6:5C:AD:BB:59:AD:B6:EF:E8:4D:A2:23:11:B3:56:24:B7:1B:3B:1E:A0:DA:8B:66:55:17:4E:C8:97:86:46+-----BEGIN CERTIFICATE-----+MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBjMQswCQYDVQQG+EwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBHbWJIMSswKQYDVQQDDCJU+ZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAyMDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMy+NzIzNTk1OVowYzELMAkGA1UEBhMCREUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJp+dHkgR21iSDErMCkGA1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIw+DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9cUD/h3VC+KSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHVcp6R+SPWcHu79ZvB7JPP+GeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMAU6DksquDOFczJZSfvkgdmOGjup5czQRx+UX11eKvzWarE4GC+j4NSuHUaQTXtvPM6Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWo+l8hHD/BeEIvnHRz+sTugBTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9+FIS3R/qy8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73Jco4v+zLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg8qKrBC7m8kwOFjQg+rIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8rFEz0ciD0cmfHdRHNCk+y7AO+oML+KFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7S+WWO/gLCMk3PLNaaZlSJhZQNg+y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNV+HQ4EFgQUtqeXgj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2+p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQpGv7qHBFfLp++sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm9S3ul0A8Yute1hTWjOKWi0Fp+kzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErwM807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy+/SKE8YXJN3nptT+/XOR0so8RYgDdGGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4+mZqTuXNnQkYRIer+CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtz+aL1txKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+w6jv/naa+oqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aKL4x35bcF7DvB7L6Gs4a8+wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+ljX273CXE2whJdV/LItM3z7gLfEdxquVeE+HVlNjM7IDiPCtyaaEBRx/pOyiriA8A4QntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0+o82bNSQ3+pCTE4FCxpgmdTdmQRCsu/WU48IxK63nI1bMNSWSs1A=+-----END CERTIFICATE-----++TWCA CYBER Root CA+==================+# Fingerprint (SHA-256): 3F:63:BB:28:14:BE:17:4E:C8:B6:43:9C:F0:8D:6D:56:F0:B7:C4:05:88:3A:56:48:A3:34:42:4D:6B:3E:C5:58+-----BEGIN CERTIFICATE-----+MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQMQswCQYDVQQG+EwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NB+IENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQG+EwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NB+IENZQkVSIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1s+Ts6P40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxFavcokPFh+V8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/34bKS1PE2Y2yHer43CdT+o0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684iJkXXYJndzk834H/nY62wuFm40AZoNWDT+Nq5xQwTxaWV4fPMf88oon1oglWa0zbfuj3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK+/c/WMw+f+5eesRycnupfXtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkH+IuNZW0CP2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDAS9TM+fAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDAoS/xUgXJP+92ZuJF+2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzCkHDXShi8fgGwsOsVHkQGzaRP6AzR+wyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAO+BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83+QOGt4A1WNzAdBgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB+AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0ttGlTITVX1olN+c79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn68xDiBaiA9a5F/gZbG0jAn/x+X9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNnTKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDR+IG4kqIQnoVesqlVYL9zZyvpoBJ7tRCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq+/p1hvIbZv97Tujqxf36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0R+FxbIQh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz8ppy6rBe+Pm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4NxKfKjLji7gh7MMrZQzv+It6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzXxeSDwWrruoBa3lwtcHb4yOWHh8qgnaHl+IhInD0Q9HWzq1MKLL295q39QpsQZp6F6t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X+-----END CERTIFICATE-----++SecureSign Root CA14+====================+# Fingerprint (SHA-256): 4B:00:9C:10:34:49:4F:9A:B5:6B:BA:3B:A1:D6:27:31:FC:4D:20:D8:95:5A:DC:EC:10:A9:25:60:72:61:E3:38+-----BEGIN CERTIFICATE-----+MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEMBQAwUTELMAkG+A1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBMdGQuMR0wGwYDVQQDExRT+ZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgwNzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJ+BgNVBAYTAkpQMSMwIQYDVQQKExpDeWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMU+U2VjdXJlU2lnbiBSb290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh+1oq/FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOgvlIfX8xn+bacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy6pJxaeQp8E+BgQQ8sqVb+1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa+/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9JkdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOE+kJTRX45zGRBdAuVwpcAQ0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSx+jVIHvXiby8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac18iz+ju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs0Wq2XSqypWa9a4X0+dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIABSMbHdPTGrMNASRZhdCyvjG817XsY+AFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVLApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQAB+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeq+YR3r6/wtbyPk86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E+rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ibed87hwriZLoA+ymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopTzfFP7ELyk+OZpDc8h7hi2/Ds+Hzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHSDCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPG+FrojutzdfhrGe0K22VoF3Jpf1d+42kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6q+nsb58Nn4DSEC5MUoFlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/+OfVyK4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6dB7h7sxa+OgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtlLor6CZpO2oYofaphNdgO+pygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB365jJ6UeTo3cKXhZ+PmhIIynJkBugnLN+eLLIjzwec+fBH7/PzqUqm9tEZDKgu39cJRNItX+S+-----END CERTIFICATE-----++SecureSign Root CA15+====================+# Fingerprint (SHA-256): E7:78:F0:F0:95:FE:84:37:29:CD:1A:00:82:17:9E:53:14:A9:C2:91:44:28:05:E1:FB:1D:8F:B6:B8:88:6C:3A+-----BEGIN CERTIFICATE-----+MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMwUTELMAkGA1UE+BhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBMdGQuMR0wGwYDVQQDExRTZWN1+cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMyNTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNV+BAYTAkpQMSMwIQYDVQQKExpDeWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2Vj+dXJlU2lnbiBSb290IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5G+dCx4wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSRZHX+AezB+2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD+AgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT9DAKBggqhkjOPQQDAwNoADBlAjEA2S6J+fl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJ+SwdLZrWeqrqgHkHZAXQ6bkU6iYAZezKYVWOr62Nuk22rGwlgMU4=+-----END CERTIFICATE-----++D-TRUST BR Root CA 2 2023+=========================+# Fingerprint (SHA-256): 05:52:E6:F8:3F:DF:65:E8:FA:96:70:E6:66:DF:28:A4:E2:13:40:B5:10:CB:E5:25:66:F9:7C:4F:B9:4B:2B:D1+-----BEGIN CERTIFICATE-----+MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBIMQswCQYDVQQG+EwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRSVVNUIEJSIFJvb3QgQ0Eg+MiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUwOTA4NTYzMFowSDELMAkGA1UEBhMCREUxFTAT+BgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDIgMjAyMzCC+AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK7/CVmRgApKaOYkP7in5Mg6CjoWzckjYaCT+cfKri3OPoGdlYNJUa2NRb0kz4HIHE304zQaSBylSa053bATTlfrdTIzZXcFhfUvnKLNEgXtRr90z+sWh81k5M/itoucpmacTsXld/9w3HnDY25QdgrMBM6ghs7wZ8T1soegj8k12b9py0i4a6Ibn08OhZ+WiihNIQaJZG2tY/vsvmA+vk9PBFy2OMvhnbFeSzBqZCTRphny4NqoFAjpzv2gTng7fC5v2Xx2Mt6+++9zA84A9H3X4F07ZrjcjrqDy4d2A/wl2ecjbwb9Z/Pg/4S8R7+1FhhGaRTMBffb00msa8yr5LUL+QyReS2tNZ9/WtT5PeB+UcSTq3nD88ZP+npNa5JRal1QMNXtfbO4AHyTsA7oC9Xb0n9Sa7YUsOCIv+x9gvdhFP/Wxc6PWOJ4d/GUohR5AdeY0cW/jPSoXk7bNbjb7EZChdQcRurDhaTyN0dKkSw/bSuREV+MweR2Ds3OmMwBtHFIjYoYiMQ4EbMl6zWK11kJNXuHA7e+whadSr2Y23OC0K+0bpwHJwh5Q8xaRfX+/Aq03u2AnMuStIv13lmiWAmlY0cL4UEyNEHZmrHZqLAbWt4NDfTisl01gLmB1IRpkQLLddCNxbU9+CZEJjxShFHR5PtbJFR2kWVki3PaKRT08EtY+XTIvAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB+/zAdBgNVHQ4EFgQUZ5Dw1t61GNVGKX5cq/ieCLxklRAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRC+MEAwPqA8oDqGOGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfYnJfcm9vdF9jYV8y+XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA097N3U9swFrktpSHxQCF16+tIFoE9c+CeJyrr+d6kTpGoKWloUMz1oH4Guaf2Mn2VsNELZLdB/eBaxOqwjMa1ef67nriv6uvw8l5VAk1/DLQOj7aRv+U9f6QA4w9QAgLABMjDu0ox+2v5Eyq6+SmNMW5tTRVFxDWy6u71cqqLRvpO8NVhTaIasgdp4D/Ca4+nj8+AybmTNudX0KEPUUDAxxZiMrcLmEkWqTqJwtzEr5SswrPMhfiHocaFpVIbVrg0M8JkiZmkdij+YQ6qgYF/6FKC0ULn4B0Y+qSFNueG4A3rvNTJ1jxD8V1Jbn6Bm2m1iWKPiFLY1/4nwSPFyysCu7Ff+/vtDhQNGvl3GyiEm/9cCnnRK3PgTFbGBVzbLZVzRHTF36SXDw7IyN9XxmAnkbWOACKsGkoHU6XCP+pz+y7YaMgmo1yEJagtFSGkUPFaUA8JR7ZSdXOUPPfH/mvTWze/EZTN46ls/pdu4D58JDUjxqgejB+WoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aSEcr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/+5usWDiJFAbzdNpQ0qTUmiteXue4Icr80knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jt+n/mtd+ArY0+ew+43u3gJhJ65bvspmZDogNOfJA==+-----END CERTIFICATE-----++TrustAsia TLS ECC Root CA+=========================+# Fingerprint (SHA-256): C0:07:6B:9E:F0:53:1F:B1:A6:56:D6:7C:4E:BE:97:CD:5D:BA:A4:1E:F4:45:98:AC:C2:48:98:78:C9:2D:87:11+-----BEGIN CERTIFICATE-----+MIICMTCCAbegAwIBAgIUNnThTXxlE8msg1UloD5Sfi9QaMcwCgYIKoZIzj0EAwMwWDELMAkGA1UE+BhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMsIEluYy4xIjAgBgNVBAMTGVRy+dXN0QXNpYSBUTFMgRUNDIFJvb3QgQ0EwHhcNMjQwNTE1MDU0MTU2WhcNNDQwNTE1MDU0MTU1WjBY+MQswCQYDVQQGEwJDTjElMCMGA1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAG+A1UEAxMZVHJ1c3RBc2lhIFRMUyBFQ0MgUm9vdCBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLh/+pVs/AT598IhtrimY4ZtcU5nb9wj/1WrgjstEpvDBjL1P1M7UiFPoXlfXTr4sP/MSpwDpguMqWzJ8+S5sUKZ74LYO1644xST0mYekdcouJtgq7nDM1D9rs3qlKH8kzsaNCMEAwDwYDVR0TAQH/BAUwAwEB+/zAdBgNVHQ4EFgQULIVTu7FDzTLqnqOH/qKYqKaT6RAwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49+BAMDA2gAMGUCMFRH18MtYYZI9HlaVQ01L18N9mdsd0AaRuf4aFtOJx24mH1/k78ITcTaRTChD15K+eAIxAKORh/IRM4PDwYqROkwrULG9IpRdNYlzg8WbGf60oenUoWa2AaU2+dhoYSi3dOGiMQ==+-----END CERTIFICATE-----++TrustAsia TLS RSA Root CA+=========================+# Fingerprint (SHA-256): 06:C0:8D:7D:AF:D8:76:97:1E:B1:12:4F:E6:7F:84:7E:C0:C7:A1:58:D3:EA:53:CB:E9:40:E2:EA:97:91:F4:C3+-----BEGIN CERTIFICATE-----+MIIFgDCCA2igAwIBAgIUHBjYz+VTPyI1RlNUJDxsR9FcSpwwDQYJKoZIhvcNAQEMBQAwWDELMAkG+A1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMsIEluYy4xIjAgBgNVBAMT+GVRydXN0QXNpYSBUTFMgUlNBIFJvb3QgQ0EwHhcNMjQwNTE1MDU0MTU3WhcNNDQwNTE1MDU0MTU2+WjBYMQswCQYDVQQGEwJDTjElMCMGA1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEi+MCAGA1UEAxMZVHJ1c3RBc2lhIFRMUyBSU0EgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIP+ADCCAgoCggIBAMMWuBtqpERz5dZO9LnPWwvB0ZqB9WOwj0PBuwhaGnrhB3YmH49pVr7+NmDQDIPN+lOrnxS1cLwUWAp4KqC/lYCZUlviYQB2srp10Zy9U+5RjmOMmSoPGlbYJQ1DNDX3eRA5gEk9bNb2/+mThtfWza4mhzH/kxpRkQcwUqwzIZheo0qt1CHjCNP561HmHVb70AcnKtEj+qpklz8oYVlQwQX1Fk+zv93uMltrOXVmPGZLmzjyUT5tUMnCE32ft5EebuyjBza00tsLtbDeLdM1aTk2tyKjg7/D8OmYCYo+zza/+lcK7Fs/6TAWe8TbxNRkoDD75f0dcZLdKY9BWN4ArTr9PXwaqLEX8E40eFgl1oUh63kd0Nyr+z2I8sMeXi9bQn9P+PN7F4/w6g3CEIR0JwqH8uyghZVNgepBtljhb//HXeltt08lwSUq6HTrQUNoy+IBnkiz/r1RYmNzz7dZ6wB3C4FGB33PYPXFIKvF1tjVEK2sUYyJtt3LCDs3+jTnhMmCWr8n4uIF6C+FabW2I+s5c0yhsj55NqJ4js+k8UTav/H9xj8Z7XvGCxUq0DTbE3txci3OE9kxJRMT6DNrqXGJyV1+J23G2pyOsAWZ1SgRxSHUuPzHlqtKZFlhaxP8S8ySpg+kUb8OWJDZgoM5pl+z+m6Ss80zDoWo8SnT+q1mt1tve1CuBAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLgHkXlcBvRG/XtZ+ylomkadFK/hTMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAIZtqBSBdGBanEqT3+Rz/NyjuujsCCztxIJXgXbODgcMTWltnZ9r96nBO7U5WS/8+S4PPFJzVXqDuiGev4iqME3mmL5Dw8+veWv0BIb5Ylrc5tvJQJLkIKvQMKtuppgJFqBTQUYo+IzeXoLH5Pt7DlK9RME7I10nYEKqG/odv6L+TytpEoYKNDbdgptvT+Bz3Ul/KD7JO6NXBNiT2Twp2xIQaOHEibgGIOcberyxk2GaGUARtWqFVwHx+tlotJnMnlvm5P1vQiJ3koP26TpUJg3933FEFlJ0gcXax7PqJtZwuhfG5WyRasQmr2soaB82G39tp+27RIGAAtvKLEiUUjpQ7hRGU+isFqMB3iYPg6qocJQrmBktwliJiJ8Xw18WLK7nn4GS/+X/jbh87q+qA8MpugLoDzga5SYnH+tBuYc6kIQX+ImFTw3OffXvO645e8D7r0i+yiGNFjEWn9hongPXvPKnbwb+PKfILfanIhHKA9jnZwqKDss1jjQ52MjqjZ9k4DewbNfFj8GQYSbbJIweSsCI3zWQzj8C9GRh3sfI+B5XeMhg6j6JCQCTl1jNdfK7vsU1P1FeQNWrcrgSXSYk0ly4wBOeY99sLAZDBHwo/+ML+TvrbmnNz+FrwFuHnYWa8G5z9nODmxfKuU4CkUpijy323imttUQ/hHWKNddBWcwauwxzQ=+-----END CERTIFICATE-----++D-TRUST EV Root CA 2 2023+=========================+# Fingerprint (SHA-256): 8E:82:21:B2:E7:D4:00:78:36:A1:67:2F:0D:CC:29:9C:33:BC:07:D3:16:F1:32:FA:1A:20:6D:58:71:50:F1:CE+-----BEGIN CERTIFICATE-----+MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBIMQswCQYDVQQG+EwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRSVVNUIEVWIFJvb3QgQ0Eg+MiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUwOTA5MTAzMlowSDELMAkGA1UEBhMCREUxFTAT+BgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDIgMjAyMzCC+AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANiOo4mAC7JXUtypU0w3uX9jFxPvp1sjW2l1+sJkKF8GLxNuo4MwxusLyzV3pt/gdr2rElYfXR8mV2IIEUD2BCP/kPbOx1sWy/YgJ25yE7CUXFId/+MHibaljJtnMoPDT3mfd/06b4HEV8rSyMlD/YZxBTfiLNTiVR8CUkNRFeEMbsh2aJgWi6zCudR3Mf+vc2RpHJqnKIbGKBv7FD0fUDCqDDPvXPIEysQEx6Lmqg6lHPTGGkKSv/BAQP/eX+1SH977ugpbzZM+lWGG2Pmic4ruri+W7mjNPU0oQvlFKzIbRlUWaqZLKfm7lVa/Rh3sHZMdwGWyH6FDrlaeoLGPaxK3+YG14C8qKXO0elg6DpkiVjTujIcSuWMYAsoS0I6SWhjW42J7YrDRJmGOVxcttSEfi8i4YHtAxq910+7PncjLgcjmgjutDzUNzPZY9zOjLHfP7KgiJPvo5iR2blzYfi6NUPGJ/lBHJLRjwQ8kTCZFZxTnXo+nMkmdMV9WdEKWw9t/p51HBjGGjp82A0EzM23RWV6sY+4roRIPrN6TagD4uJ+ARZZaBhDM7DS3LAa+QzXupdqpRlyuhoFBAUp0JuyfBr/CBTdkdXgpaP3F9ev+R/nkhbDhezGdpn9yo7nELC7MmVcOIQxF+AZRl62UJxmMiCzNJkkg8/M3OsD6Onov4/knFNXJHAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB+/zAdBgNVHQ4EFgQUqvyREBuHkV8Wub9PS5FeAByxMoAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRC+MEAwPqA8oDqGOGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfZXZfcm9vdF9jYV8y+XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQCTy6UfmRHsmg1fLBWTxj++EI14QvBukEdHjqOS+Mo1wj/Zbjb6JzkcBahsgIIlbyIIQbODnmaprxiqgYzWRaoUlrRc4pZt+UPJ26oUFKidBK7GB0aL2+QHWpDsvxVUjY7NHss+jOFKE17MJeNRqrphYBBo7q3C+jisosketSjl8MmxfPy3MHGcRqwnNU73xD+UmPBEcrCRbH0O1P1aa4846XerOhUt7KR/aypH/KH5BfGSah82ApB9PI+53c0BFLd6IHyTS9URZ0V+4U/M5d40VxDJI3IXcI1QcB9WbMy5/zpaT2N6w25lBx2Eof+pDGOJbbJAiDnXH3dotfyc1dZnaVuo+dNv8ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT2vFp4LJi+TZ6P/4mdm13NRemUAiKN4DV/6PEEeXFsVIP4M7kFMhtYVRFP0OUnR3Hs7dpn1mKmS00PaaLJvOwi+S5THaJQXfuKOKD62xur1NGyfN4gHONuGcfrNlUhDbqNPgofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/+HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAstNl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L++KIkBI3Y4WNeApI02phhXBxvWHZks/wCuPWdCg==+-----END CERTIFICATE-----++SwissSign RSA TLS Root CA 2022 - 1+==================================+# Fingerprint (SHA-256): 19:31:44:F4:31:E0:FD:DB:74:07:17:D4:DE:92:6A:57:11:33:88:4B:43:60:D3:0E:27:29:13:CB:E6:60:CE:41+-----BEGIN CERTIFICATE-----+MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQELBQAwUTELMAkG+A1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UEAxMiU3dpc3NTaWduIFJTQSBU+TFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgxMTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJ+BgNVBAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0Eg+VExTIFJvb3QgQ0EgMjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmji+C8NXvDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7LCTLf5Im+gKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX5XH8irCRIFucdFJtrhUn+WXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyEEPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlf+GUEGjw5NBuBwQCMBauTLE5tzrE0USJIt/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36q+OTw7D59Ke4LKa2/KIj4x0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLO+EGrOyvi5KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM0ZPl+EuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shdOxtYk8EXlFXIC+OC+eYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrtaclXvyFu1cvh43zcgTFeRc5JzrBh3+Q4IgaezprClG5QtO+DdziZaKHG29777YtvTKwP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQAB+o2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow+4UD2p8P98Q+4DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL+BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO310aewCoSPY6W+lkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgzHqp41eZUBDqyggmNzhYzWUUo+8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQiJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zp+y1FVCypM9fJkT6lc/2cyjlUtMoIcgC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3Cjlvr+zG4ngRhZi0Rjn9UMZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6M+OuhFLhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJpzv1/THfQ+wUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/TdAo9QAwKxuDdollDruF/U+KIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0n+hzck5npgL7XTgwSqT0N1osGDsieYK7EOgLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rw+tnu64ZzZ+-----END CERTIFICATE-----++OISTE Server Root ECC G1+========================+# Fingerprint (SHA-256): EE:C9:97:C0:C3:0F:21:6F:7E:3B:8B:30:7D:2B:AE:42:41:2D:75:3F:C8:21:9D:AF:D1:52:0B:25:72:85:0F:49+-----BEGIN CERTIFICATE-----+MIICNTCCAbqgAwIBAgIQI/nD1jWvjyhLH/BU6n6XnTAKBggqhkjOPQQDAzBLMQswCQYDVQQGEwJD+SDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwYT0lTVEUgU2VydmVyIFJvb3Qg+RUNDIEcxMB4XDTIzMDUzMTE0NDIyOFoXDTQ4MDUyNDE0NDIyN1owSzELMAkGA1UEBhMCQ0gxGTAX+BgNVBAoMEE9JU1RFIEZvdW5kYXRpb24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IEVDQyBH+MTB2MBAGByqGSM49AgEGBSuBBAAiA2IABBcv+hK8rBjzCvRE1nZCnrPoH7d5qVi2+GXROiFPqOuj+vqQycvO2Ackr/XeFblPdreqqLiWStukhEaivtUwL85Zgmjvn6hp4LrQ95SjeHIC6XG4N2xml4z+c+KrhAS93mT6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQ3TYhlz/w9itWj8UnATgwQ+b0K0nDAdBgNVHQ4EFgQUN02IZc/8PYrVo/FJwE4MEG9CtJwwDgYDVR0PAQH/BAQDAgGGMAoGCCqG+SM49BAMDA2kAMGYCMQCpKjAd0MKfkFFRQD6VVCHNFmb3U2wIFjnQEnx/Yxvf4zgAOdktUyBFCxxg+ZzFDJe0CMQCSia7pXGKDYmH5LVerVrkR3SW+ak5KGoJr3M/TvEqzPNcum9v4KGm8ay3sMaE641c=+-----END CERTIFICATE-----++OISTE Server Root RSA G1+========================+# Fingerprint (SHA-256): 9A:E3:62:32:A5:18:9F:FD:DB:35:3D:FD:26:52:0C:01:53:95:D2:27:77:DA:C5:9D:B5:7B:98:C0:89:A6:51:E6+-----BEGIN CERTIFICATE-----+MIIFgzCCA2ugAwIBAgIQVaXZZ5Qoxu0M+ifdWwFNGDANBgkqhkiG9w0BAQwFADBLMQswCQYDVQQG+EwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwYT0lTVEUgU2VydmVyIFJv+b3QgUlNBIEcxMB4XDTIzMDUzMTE0MzcxNloXDTQ4MDUyNDE0MzcxNVowSzELMAkGA1UEBhMCQ0gx+GTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRpb24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IFJT+QSBHMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKqu9KuCz/vlNwvn1ZatkOhLKdxV+YOPMvLO8LZK55KN68YG0nnJyQ98/qwsmtO57Gmn7KNByXEptaZnwYx4M0rH/1ow00O7brEi56rAU+jtgHqSSY3ekJvqgiG1k50SeH3BzN+Puz6+mTeO0Pzjd8JnduodgsIUzkik/HEzxux9UTl7Ko2yRp+g1bTacuCErudG/L4NPKYKyqOBGf244ehHa1uzjZ0Dl4zO8vbUZeUapU8zhhabkvG/AePLhq5Svdk+NCncpo1Q4Y2LS+VIG24ugBA/5J8bZT8RtOpXaZ+0AOuFJJkk9SGdl6r7NH8CaxWQrbueWhl/pIzY++m0o/DjH40ytas7ZTpOSjswMZ78LS5bOZmdTaMsXEY5Z96ycG7mOaES3GK/m5Q9l3JUJsJMStR8++lKXHiHUhsd4JJCpM4rzsTGdHwimIuQq6+cF0zowYJmXa92/GjHtoXAvuY8BeS/FOzJ8vD+HomnqT+8eDI278n5mUpezbgMxVz8p1rhAhoKzYHKyfMeNhqhw5HdPSqoBNdZH702xSu+zrkL8Fl47l6QGzw+Brd7KJvX4V84c5Ss2XCTLdyEr0YconosP4EmQufU2MVshGYRi3drVByjtdgQ8K4p92cIiBdcuJd5+z+orKu5YM+Vt6SmqZQENghPsJQtdLEByFSnTkCz3GkPVavBpAgMBAAGjYzBhMA8GA1UdEwEB/wQF+MAMBAf8wHwYDVR0jBBgwFoAU8snBDw1jALvsRQ5KH7WxszbNDo0wHQYDVR0OBBYEFPLJwQ8NYwC7+7EUOSh+1sbM2zQ6NMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQwFAAOCAgEANGd5sjrG5T33+I3K5Ce+SrScfoE4KsvXaFwyihdJ+klH9FWXXXGtkFu6KRcoMQzZENdl//nk6HOjG5D1rd9QhEOP2+8yBOqb6J8xycqd+8MDoX0TJD0KqKchxRKEzdNsjkLWd9kYccnbz8qyiWXmFcuCIzGEgWUOrKL+ml+Sdx/PKQZvDatkuK59EvV6wit53j+F8Bdh3foZ3dPAGav9LEDOr4SfEE15fSmG0eLy3n31r8Xbk5l+8PjaV8GUgeV6Vg27Rn9vkf195hfkgSe7BYhW3SCl95gtkRlpMV+bMPKZrXJAlszYd2abtNUOshD++FKrDgHGdPY3ofRRsYWSGRqbXVMW215AWRqWFyp464+YTFrYVI8ypKVL9AMb2kI5Wj4kI3Zaq5tNq+qYY19tVFeEJKRvwDyF7YZvZFZSS0vod7VSCd9521Kvy5YhnLbDuv0204bKt7ph6N/Ome/msVuduC+msuY33OhkKCgxeDoAaijFJzIwZqsFVAzje18KotzlUBDJvyBpCpfOZC3J8tRd/iWkx7P8nd9H0aT+olkelUTFLXVksNb54Dxp6gS1HAviRkRNQzuXSXERvSS2wq1yVAb+axj5d9spLFKebXd7Yv0PTY6Y+MjAwcRLWJTXjn/hvnLXrahut6hDTlhZyBiElxky8j3C7DOReIoMt0r7+hVu05L0=+-----END CERTIFICATE-----++e-Szigno TLS Root CA 2023+=========================+# Fingerprint (SHA-256): B4:91:41:50:2D:00:66:3D:74:0F:2E:7E:C3:40:C5:28:00:96:26:66:12:1A:36:D0:9C:F7:DD:2B:90:38:4F:B4+-----BEGIN CERTIFICATE-----+MIICzzCCAjGgAwIBAgINAOhvGHvWOWuYSkmYCjAKBggqhkjOPQQDBDB1MQswCQYDVQQGEwJIVTER+MA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xFzAVBgNVBGEMDlZBVEhV+LTIzNTg0NDk3MSIwIAYDVQQDDBllLVN6aWdubyBUTFMgUm9vdCBDQSAyMDIzMB4XDTIzMDcxNzE0+MDAwMFoXDTM4MDcxNzE0MDAwMFowdTELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRYw+FAYDVQQKDA1NaWNyb3NlYyBMdGQuMRcwFQYDVQRhDA5WQVRIVS0yMzU4NDQ5NzEiMCAGA1UEAwwZ+ZS1Temlnbm8gVExTIFJvb3QgQ0EgMjAyMzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAGgP36J8+PKp0iGEKjcJMpQEiFNT3YHdCnAo4YKGMZz6zY+n6kbCLS+Y53wLCMAFSAL/fjO1ZrTJlqwlZULUZ+wmgcAOAFX9pQJhzDrAQixTpN7+lXWDajwRlTEArRzT/vSzUaQ49CE0y5LBqcvjC2xN7cS53kpDzL+Ltmt3999Cd8ukv+ho2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4E+FgQUWYQCYlpGePVd3I8KECgj3NXW+0UwHwYDVR0jBBgwFoAUWYQCYlpGePVd3I8KECgj3NXW+0Uw+CgYIKoZIzj0EAwQDgYsAMIGHAkIBLdqu9S54tma4n7Zwf2Z0z+yOfP7AAXmazlIC58PRDHpty7Ve+7hekm9sEdu4pKeiv+62sUvTXK9Z3hBC9xdIoaDQCQTV2WnXzkoYI9bIeCvZlC9p2x1L/Cx6AcCIw+wzPbGO2E14vs7dOoY4G1VnxHx1YwlGhza9IuqbnZLBwpvQy6uWWL+-----END CERTIFICATE-----++SECOM TLS RSA Root CA 2024+==========================+# Fingerprint (SHA-256): 14:35:F2:25:C5:D2:52:D7:A2:19:48:CC:3C:E6:2A:EC:FA:88:00:1E:3D:D7:2D:1C:C3:55:51:00:EB:37:2F:93+-----BEGIN CERTIFICATE-----+MIIFmjCCA4KgAwIBAgIJAO6JNNDLgOCyMA0GCSqGSIb3DQEBDAUAMFoxCzAJBgNVBAYTAkpQMSYw+JAYDVQQKEx1TRUNPTSBUcnVzdCBTeXN0ZW1zIENvLiwgTHRkLjEjMCEGA1UEAxMaU0VDT00gVExT+IFJTQSBSb290IENBIDIwMjQwHhcNMjQwMTMxMDUxMTU1WhcNNDkwMTE0MDUxMTU1WjBaMQswCQYD+VQQGEwJKUDEmMCQGA1UEChMdU0VDT00gVHJ1c3QgU3lzdGVtcyBDby4sIEx0ZC4xIzAhBgNVBAMT+GlNFQ09NIFRMUyBSU0EgUm9vdCBDQSAyMDI0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC+AgEA4TjizUwzxbInq8Tx11gaFYNk5fO+34y7TyM4neh0UgL5JIZbJNLTz2x//L/B71+5m6X6nGIr+7d4lFJBGtjO677hXOz93zkcWaUTm3VbOAjBlt4YWxlcccBHXuZ7o3Q+4R+ormrBdHeJ1CTUEG8tt+QbKIl3G7OZYbnH8/pP8cjPub/0kDVNuMzp7xsVRROOisQt53fMoJLlYgoebbuMphOqMCtjkJ7R6e+fEMfLp8UAVi9ZaLRn76ET/CJkk925nduuufC4BatS4mnXFmxN0vUXb0ij9B8O/D8gixQEsVSD4GK+8FWRPh3bVd/6bzdkHGJjy21XI0yejVomZUbRrOfNuz0boPGV1pt18fFC39IHQEth3OFqb5NDO3L++A9bNqTgAyUgRmIn4ucgDc/Ri/Km3V51ueZjy1/yk0qwJVadAVVrCt56iNeXOyEvzJADGgDQ8E1Pd+aqct8Cynz/47ReQM62vFYO08wcQkrjmX/tesiko1V1yyaf6EfPzUFzmaGy9xvkCwdbm15EdTolOj+E0H2Vb5/APDOyCFEokiYGmXTLdAUl0wKZ4IyjkHGzy0jhpaXEXE/GJcEvI6VzEchjaBL03EJ0h9p+G4OqeIOycKvAo3A+TbetyfsrgYyHzU0a7/qUjGat1AAq1nVljMpKqpinPTsf/d9H39FTUeJL7Tpz+zjUCAwEAAaNjMGEwHQYDVR0OBBYEFCzrchKOWHdkNRVWNQFXB6l9DTbmMB8GA1UdIwQYMBaAFCzr+chKOWHdkNRVWNQFXB6l9DTbmMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqG+SIb3DQEBDAUAA4ICAQAVwsvluSafaez5tFPR/hRTBzRxEyMMQF3XJXCVi3yegZyKoec7hmE6jx2Z+M8KgM1kn2yJRwFXHX8zUW9nBLEDWc4wuE8LrlZqhGZM9pJQXGmGzResDJV6JgRBna+j4sA1M7yId+lvL0sAfFXFCTRaWTD4E1V99RLrFzWfTcC+e180hDuNMpqOEo46+lMeW/Wvh7ifOQs+kiK0O2gHxQ+DNxslSavnCs4V7l8HRDJ2La10o70Bo7VLzf1W8MBvv0VTnxB+NjT5qTAbhGFh9Gvp4BaJpmdUf0C+5CEP6dbQlfgxWfzYr69yVT6dPQB+GFEaY03IMY+AcBCs+om1fNxrQXt9zoofMBNFbLhvpNH/JsXW+dGUzfNbO12uswTa5wah8LB18FTQN2/zPHYmvBEoLuyUgZ09VNLJo5YA0kXItVYkLjMe2SixzK4sc+UHv81IK99I91DWx7FwMVKw2xgFp+ZLYB2dnpQQrqwlW64glHUcK2N9BDsnjLSxeZ+UPECh9RxH4W+AcKiZW+cqaKMmhP2WBfR4IcR7NOL32ml11ds87hhV1CZWWFCJAcCidYZz6CZa8exzHojP9SB5RH0+/v1KdHAisqhSjtJl/UIAHIQ48elOn8wrTdFap4Yb5aHglmMeNx+fAIhDluWVfxTO7H4dTPU+SFVR+MLAh+wwKZfqb94nMeQ==+-----END CERTIFICATE-----++SECOM TLS ECC Root CA 2024+==========================+# Fingerprint (SHA-256): 6A:B2:AB:75:F5:1C:B4:F4:F0:15:62:03:FB:F6:F6:46:23:2F:51:4B:E0:59:F6:28:33:30:8B:82:B4:D7:2D:B1+-----BEGIN CERTIFICATE-----+MIICTDCCAdGgAwIBAgIJAIF6LO+PI3pEMAoGCCqGSM49BAMDMFoxCzAJBgNVBAYTAkpQMSYwJAYD+VQQKEx1TRUNPTSBUcnVzdCBTeXN0ZW1zIENvLiwgTHRkLjEjMCEGA1UEAxMaU0VDT00gVExTIEVD+QyBSb290IENBIDIwMjQwHhcNMjQwMTMxMDU1MjM0WhcNNDkwMTE0MDU1MjM0WjBaMQswCQYDVQQG+EwJKUDEmMCQGA1UEChMdU0VDT00gVHJ1c3QgU3lzdGVtcyBDby4sIEx0ZC4xIzAhBgNVBAMTGlNF+Q09NIFRMUyBFQ0MgUm9vdCBDQSAyMDI0MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE7NzFMtu9dzQX+SNC12fabk0+GlC5finB3R7XaZonRUd20aFiWObtuNBCLUZSfk6QXAE55BjEXsXQ/NG8yUqicXjsu+9ksDK3JZBgCwLOVh6+nwJXTvso/dEj/GUYH5mBdoo2MwYTAdBgNVHQ4EFgQUO3YReyl04k4GTFaC+QNAhL3qzydUwHwYDVR0jBBgwFoAUO3YReyl04k4GTFaCQNAhL3qzydUwDgYDVR0PAQH/BAQDAgEG+MA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwMDaQAwZgIxAN3ib8fi1pMYtAPjMilB5e5/H+t5+CL0xPL+cZ5oTTZuSCjpAn1v7F/VAr8bFxQXAowIxAKsBVO1ACFp7skwzPvdv1EUY5a897WGLT4lb++bjxFAWyl8wDcZJdwGZ/pAHxt1AJ1g==+-----END CERTIFICATE-----++Telia EC TLS Root CA v3+=======================+# Fingerprint (SHA-256): 09:8E:08:A9:1D:BB:F7:74:78:B9:6C:CE:B8:9B:14:13:A5:DA:37:B7:C8:62:60:6A:95:5D:EB:07:17:9F:43:26+-----BEGIN CERTIFICATE-----+MIICMjCCAbegAwIBAgIPAYvSIlRjTQSLbOVHH9K1MAoGCCqGSM49BAMDMEoxCzAJBgNVBAYTAlNF+MRkwFwYDVQQKDBBUZWxpYSBDb21wYW55IEFCMSAwHgYDVQQDDBdUZWxpYSBFQyBUTFMgUm9vdCBD+QSB2MzAeFw0yMzExMTUwODU1MjZaFw00ODA1MjMxMTAwMDBaMEoxCzAJBgNVBAYTAlNFMRkwFwYD+VQQKDBBUZWxpYSBDb21wYW55IEFCMSAwHgYDVQQDDBdUZWxpYSBFQyBUTFMgUm9vdCBDQSB2MzB2+MBAGByqGSM49AgEGBSuBBAAiA2IABMHIlhVDLbmFKUpW0iK4dpryT6emYOeS31JPwWnWPmkWRrAk+TbPX40sQfHI9mpR7Rbktu3ngg6W+BBSXSechtMCnBmWXj/EaVlmV5cY1jD2HoTfhBQ3AacpCNMLJ+K4NpZaNjMGEwHwYDVR0jBBgwFoAU1GToQ4g6cy/QGnGCNgtehd7H3kMwHQYDVR0OBBYEFNRk6EOI+OnMv0BpxgjYLXoXex95DMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49+BAMDA2kAMGYCMQCXAUdS/9bbJ8A1JYaGf/bWt/s7Ta0ot5Ulno8OjSNYRWQIlS4tVWldvTAVA7he+OFgCMQCvKr8+Z2Rn+OBr5UHzlgBObpad1LuwNTRcdNgUJxIWadcki+UBLEi1/AURKV5md2M=+-----END CERTIFICATE-----++Telia RSA TLS Root CA v3+========================+# Fingerprint (SHA-256): D1:3D:B1:29:4C:45:EB:C6:FC:86:C6:BB:F6:9F:A2:9B:DF:E6:92:DF:F7:C7:13:C2:43:C7:A9:56:C6:A2:28:4C+-----BEGIN CERTIFICATE-----+MIIFgjCCA2qgAwIBAgIPAYvSUKtCVSxHWr2h3BrFMA0GCSqGSIb3DQEBDAUAMEsxCzAJBgNVBAYT+AlNFMRkwFwYDVQQKDBBUZWxpYSBDb21wYW55IEFCMSEwHwYDVQQDDBhUZWxpYSBSU0EgVExTIFJv+b3QgQ0EgdjMwHhcNMjMxMTE1MDk0NzQyWhcNNDgwNTIzMTEwMDAwWjBLMQswCQYDVQQGEwJTRTEZ+MBcGA1UECgwQVGVsaWEgQ29tcGFueSBBQjEhMB8GA1UEAwwYVGVsaWEgUlNBIFRMUyBSb290IENB+IHYzMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAsV89KG19hCf4S1Fvk8D3TyDERhmc+vx8F7Kmb4WATx3ije1id3KHxRE0TKmcNCbAQ57bvHFEYa4hR2l20VjVadExqOW+2ld99MbEiO+jR+VOz+BbxLxJnmGwCqI+BfuTjjVReDxsxjQvjgBsClaO/sm5i70nlZcWGRtIkvWDK3NNkT5RtwXc/O+8NTFVpbUqT6cRjIj3olAblR+lRf4Ffy5o+Q9fabjYn9Z9S4itruElcEFf9Ljk7fwdTycT/rvJW9w+/B3G2a3r0f/zXNOVruIBcqE6pkSospACU2bG42fYKrbM/GWnp7u+p9Frz4jaNwpb4YHuEeS8Brat+NcP8X62jXIvvKHxlsMDJCnb4U8JzFOLsU6mohVY58BdZrvi0Gk9UOuqmgoG6dskHoksjZTlK61D/+InzmEoA1yAYJFDVysjRxDUOu9cAwANbqmq77WIFL6BpnZgVqPtMfG6wN8BrTKdapvilVsYR59BFg+IsAVBMxrGh+W+QcvmJafUpASvlArKvVG2FI4i6PiLjSBT0+6F6EQLrYqefOQF/fBNEXb+njUQ0SU+VrAqtH4Y+OjCI/a4/JJQppxeemZcQ0SUShgiI5AM5xHO5iyaUrTjYH4zxUz9j+1FEbDH/xpstr1g+XBykspup+hRTaJcbA+UbpJqtWZndAPddJmt6YJQ+dU3pDu8CAwEAAaNjMGEwHwYDVR0jBBgwFoAU+sMep0t2yKFZzBJSMFFxIbzdSkqgwHQYDVR0OBBYEFLDHqdLdsihWcwSUjBRcSG83UpKoMA4GA1Ud+DwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBDAUAA4ICAQBdYzFsNGDRk7bR+/AgRKq+5637YuOW+w6uhpoS0VnKMUpyHCwku86hEvqivakPtfmlm4bFwt++sb/8OXsWBqtfbXMaB+NDTZl8XRMJuLWOW2JrbKkRzgG0eBUcvsadG1rrhbmZqYvFXaAZO7o4TdOZzxhBB5GOAWWXB3Iera+NP4J63zyo9n8Gqw3sJBG44em5hoYjBffP+npibyslnslRi4L6xHsCYj/Pab+OlqbMCB6v+sTCLeE+IukRVzoR9aQ45pEK7Z1QBnSsbAKQtss0JKD9d/mX143H1xePjPhTXlv5JCkhrcj+SShz0P9+EHoW+e6m9lyUEOIVn0rp+yVJWNbmyDv3VkwFxHC1ApSQsgSimjGQ4wtr6cSmordYxkV+Ro8lOIIhRksXP+yDk27gW6IjUXCkZKpxFjkL3jiBSc8SkxnwCWtXg8xwNwdFVNBGLCCuJnsneYXjJNqzRqUcoGwzsv+F3Qi/ZnHUNvISdevlgIAXL4Wvrxaqvoa01wB+GCfs57RTGE4TvAGhKNKus8K3hRT1BSpigzMIRzS+xtAOrqPN6j//QSmW9f8Jcncri4j2ihSpVrFU0NdNkMhZeAKidTFPsxCVFuW4Aniz7jqiw5sWtjbQ+rlW035izIEU4sYwQoC1Nx0Svy+mMTRai50LqFQ+A1/Hq6xHHDNx7CI83d23Erw==+-----END CERTIFICATE-----
+ src/Data/Binary/Parser.hs view
@@ -0,0 +1,385 @@+{-# LANGUAGE BangPatterns #-}+-- |+-- Module      :  Data.Binary.Parser+-- Copyright   :  Daan Leijen 1999-2001, Bryan O'Sullivan 2007-2015, Winterland 2016+-- License     :  BSD3+--+-- Maintainer  :  Anton Antich <anton.antich@gmail.com>+-- Stability   :  experimental+-- Portability :  portable+--+-- This library provide parsec\/attoparsec style parsing combinators for+-- <http://hackage.haskell.org/package/binary binary>+-- package. By default, this module export combinators in "Data.Binary.Get",+-- "Data.Binary.Parser.Word8" and "Data.Binary.Parser.Numeric".+--+-- The behaviour of parsers here is different to that of the+-- similarly-named parser in Parsec, as this one is all-or-nothing.+-- To illustrate the difference, the following parser will fail under+-- Parsec given an input of @\"for\"@:+--+-- >string "foo" <|> string "for"+--+-- The reason for its failure is that the first branch is a+-- partial match, and will consume the letters @\'f\'@ and @\'o\'@+-- before failing.  In binary-parsers, the above parser will /succeed/ on+-- that input, because the failed first branch will consume nothing.+--+-- There're some redundant combinators get removed, for example:+--+-- @+-- choice == asum+-- count == replicateM+-- atEnd == isEmpty+-- take == getByteString+-- many1 == some+-- @+--+-- For fast byte set operations, please use <http://hackage.haskell.org/package/charset charset>+-- package.+--+-- It's recommanded to use 'parseOnly', 'parseDetail'... functions to run your parsers since these+-- functions are faster than binary's counter part by avoiding a small constant overhead.+-- Check 'parse' for detail.+--+-- = A few words on performance and backtracking+--+-- There's a common belief that parsers which support backtracking are slow, but it's not neccessarily+-- true in binary, because binary doesn't do book keeping if you doesn't use '<|>', 'lookAhead' or their+-- friends. Combinators in this library like 'peek', 'string'... also try to avoid backtracking so+-- it's faster to use them rather than do backtracking yourself, for example, 'peek' is faster than+-- @'lookAhead' 'getWord8'@. In practice, protocols are often designed to avoid backtracking.+-- For example, if you have following parser:+--+-- >branch1 <|> branch2 <|> (skipN 1 >> branch3)+--+-- And if you can select the right branch just by looking ahead one byte, then you can rewrite it to:+--+-- @+-- w <- peek+-- if  | w == b1 -> branch1+--     | w == b2 -> branch2+--     | w == b3 -> skipN 1 >> branch3+-- @+--+-- Binary performs as fast as a non-backtracking parser as long as you construct your parser+-- without using backtracking. And sometime backtracking is indeed neccessary, for example 'scientifically'+-- is almost impossible to implement correctly if you don't do backtracking.+--+module Data.Binary.Parser+    (+    -- * Running parsers+      Parser+    , parseOnly+    , parseLazy+    , parseDetail+    , parseDetailLazy+    , parse+    -- * Decoder conversion+    , maybeDecoder+    , eitherDecoder+    -- * Combinators+    , (<?>)+    , endOfInput+    , option+    , eitherP+    , match+    , many'+    , some'+    , sepBy+    , sepBy'+    , sepBy1+    , sepBy1'+    , manyTill+    , manyTill'+    , skipMany+    , skipMany1+    -- * Re-exports+    , module Data.Binary.Get+    , module Data.Binary.Parser.Word8+    , module Data.Binary.Parser.Numeric+    ) where++import           Control.Applicative+import           Control.Monad+import           Data.Binary.Get+import qualified Data.Binary.Get.Internal   as I+import           Data.Binary.Parser.Numeric+import           Data.Binary.Parser.Word8+import qualified Data.ByteString            as B+import qualified Data.ByteString.Lazy       as L+import qualified Data.ByteString.Lazy.Internal as L (ByteString(..))++--------------------------------------------------------------------------------++-- | Alias to 'Get' for attoparsec compatibility.+type Parser a = Get a++-- | Run a parser on 'B.ByteString'.+--+-- This function does not force a parser to consume all of its input.+-- Instead, any residual input will be discarded.  To force a parser+-- to consume all of its input, use something like this:+--+-- @parseOnly (myParser <* endOfInput)@+--+parseOnly :: Get a -> B.ByteString -> Either String a+parseOnly g bs =+    case pushEndOfInput (parse g bs) of+        Fail _ _ err -> Left err+        Done _ _ a -> Right a+        _ -> error "parseOnly: impossible error!"+{-# INLINE parseOnly #-}+++-- | Similar to 'parseOnly', but run a parser on lazy 'L.ByteString'.+--+parseLazy :: Get a -> L.ByteString -> Either String a+parseLazy g (L.Chunk bs lbs) =+    case pushEndOfInput (pushChunks (parse g bs) lbs) of+        Fail _ _ err -> Left err+        Done _ _ a -> Right a+        _ -> error "parseOnly: impossible error!"+parseLazy g L.Empty =+    case pushEndOfInput (parse g B.empty) of+        Fail _ _ err -> Left err+        Done _ _ a -> Right a+        _ -> error "parseOnly: impossible error!"+{-# INLINE parseLazy #-}++-- | Run a parser on 'B.ByteString'.+--+-- This function return full parsing results: the rest of input, stop offest and fail+-- message or parsing result.+--+-- /Since: 0.2.1.0/+--+parseDetail :: Get a+            -> B.ByteString+            -> Either (B.ByteString, ByteOffset, String) (B.ByteString, ByteOffset, a)+parseDetail g bs =+    case pushEndOfInput (parse g bs) of+        Fail rest offset err -> Left (rest, offset, err)+        Done rest offset a   -> Right (rest, offset, a)+        _ -> error "parseOnly: impossible error!"+{-# INLINE parseDetail #-}++-- | Similar to 'parseDetail', but run a parser on lazy 'L.ByteString'.+--+-- /Since: 0.2.1.0/+--+parseDetailLazy :: Get a+                -> L.ByteString+                -> Either (B.ByteString, ByteOffset, String) (B.ByteString, ByteOffset, a)+parseDetailLazy g (L.Chunk bs lbs) =+    case pushEndOfInput (pushChunks (parse g bs) lbs) of+        Fail rest offset err -> Left (rest, offset, err)+        Done rest offset a   -> Right (rest, offset, a)+        _ -> error "parseOnly: impossible error!"+parseDetailLazy g L.Empty =+    case pushEndOfInput (parse g B.empty) of+        Fail rest offset err -> Left (rest, offset, err)+        Done rest offset a   -> Right (rest, offset, a)+        _ -> error "parseOnly: impossible error!"+{-# INLINE parseDetailLazy #-}++-- | Run a 'Get' monad. See 'Decoder' for what to do next, like providing+-- input, handling decoding errors and to get the output value.+--+-- Supplying an initial chunk avoids first feeding an empty chunk to the+-- incremental decoder. Use this when input is already available; further+-- chunks/end-of-input are still handled through 'Decoder'.+--+-- /Since: 0.2.1.0/+--+parse :: Get a -> B.ByteString -> Decoder a+parse g bs = calculateOffset (loop (I.runCont g bs I.Done)) (fromIntegral $ B.length bs)+  where+    calculateOffset r !acc = case r of+        I.Done inp a -> Done inp (acc - fromIntegral (B.length inp)) a+        I.Fail inp s -> Fail inp (acc - fromIntegral (B.length inp)) s+        I.Partial k -> Partial $ \ms -> case ms of+                Nothing -> calculateOffset (k Nothing) acc+                Just i -> calculateOffset (k ms) (acc + fromIntegral (B.length i))+        I.BytesRead unused k -> calculateOffset (k $! (acc - unused)) acc++    loop r = case r of+        I.Partial k -> I.Partial $ \ms -> case ms of Just _ -> loop (k ms)+                                                     Nothing -> completeLoop (k ms)+        I.BytesRead n k -> I.BytesRead n (loop . k)+        I.Done _ _ -> r+        I.Fail _ _ -> r++    completeLoop r = case r of+        I.Partial k -> completeLoop (k Nothing)+        I.BytesRead n k -> I.BytesRead n (completeLoop . k)+        I.Fail _ _ -> r+        I.Done _ _ -> r++--------------------------------------------------------------------------------++-- | Convert a 'Decoder' value to a 'Maybe' value. A 'Partial' result+-- is treated as failure.+--+-- /Since: 0.2.3.0/+--+maybeDecoder :: Decoder r -> Maybe r+maybeDecoder (Done _ _ r) = Just r+maybeDecoder _            = Nothing+{-# INLINE maybeDecoder #-}++-- | Convert a 'Decoder' value to an 'Either' value. A 'Partial'+-- result is treated as failure.+--+-- /Since: 0.2.3.0/+--+eitherDecoder :: Decoder r -> Either String r+eitherDecoder (Done _ _ r)   = Right r+eitherDecoder (Fail _ _ msg) = Left msg+eitherDecoder _              = Left "Decoder: incomplete input"+{-# INLINE eitherDecoder #-}++--------------------------------------------------------------------------------++-- | Name the parser, in case failure occurs.+(<?>) :: Get a -> String -> Get a+(<?>) = flip label+infix 0 <?>+{-# INLINE (<?>) #-}++-- | Match only if all input has been consumed.+endOfInput :: Get ()+endOfInput = do+    e <- isEmpty+    unless e (fail "endOfInput")+{-# INLINE endOfInput #-}++-- | @option x p@ tries to apply action @p@. If @p@ fails without+-- consuming input, it returns the value @x@, otherwise the value+-- returned by @p@.+--+-- > priority  = option 0 (digitToInt <$> digit)+option :: Alternative f => a -> f a -> f a+option x p = p <|> pure x+{-# SPECIALIZE option :: a -> Get a -> Get a #-}++-- | Combine two alternatives.+eitherP :: (Alternative f) => f a -> f b -> f (Either a b)+eitherP a b = (Left <$> a) <|> (Right <$> b)+{-# INLINE eitherP #-}++-- | Return both the result of a parse and the portion of the input+-- that was consumed while it was being parsed.+match :: Get a -> Get (B.ByteString, a)+match p = do+    pos1 <- bytesRead+    (x, pos2) <- lookAhead $ (,) <$> p <*> bytesRead+    (,) <$> (getByteString . fromIntegral) (pos2 - pos1) <*> pure x+{-# INLINE match #-}++-- | A version of 'liftM2' that is strict in the result of its first+-- action.+liftM2' :: (Monad m) => (a -> b -> c) -> m a -> m b -> m c+liftM2' f a b = do+  !x <- a+  y <- b+  return (f x y)+{-# INLINE liftM2' #-}++-- | @many' p@ applies the action @p@ /zero/ or more times. Returns a+-- list of the returned values of @p@. The value returned by @p@ is+-- forced to WHNF.+--+-- >  word  = many' letter+many' :: (MonadPlus m) => m a -> m [a]+many' p = many_p+  where many_p = some_p `mplus` return []+        some_p = liftM2' (:) p many_p+{-# INLINE many' #-}++-- | @some' p@ applies the action @p@ /one/ or more times. Returns a+-- list of the returned values of @p@. The value returned by @p@ is+-- forced to WHNF.+--+-- >  word  = some' letter+some' :: (MonadPlus m) => m a -> m [a]+some' p = liftM2' (:) p (many' p)+{-# INLINE some' #-}++-- | @sepBy p sep@ applies /zero/ or more occurrences of @p@, separated+-- by @sep@. Returns a list of the values returned by @p@.+--+-- > commaSep p  = p `sepBy` (char ',')+sepBy :: Alternative f => f a -> f s -> f [a]+sepBy p s = liftA2 (:) p ((s *> sepBy1 p s) <|> pure []) <|> pure []+{-# SPECIALIZE sepBy :: Get a -> Get s -> Get [a] #-}++-- | @sepBy' p sep@ applies /zero/ or more occurrences of @p@, separated+-- by @sep@. Returns a list of the values returned by @p@. The value+-- returned by @p@ is forced to WHNF.+--+-- > commaSep p  = p `sepBy'` (char ',')+sepBy' :: (MonadPlus m) => m a -> m s -> m [a]+sepBy' p s = go `mplus` return []+  where go = liftM2' (:) p ((s >> sepBy1' p s) `mplus` return [])+{-# SPECIALIZE sepBy' :: Get a -> Get s -> Get [a] #-}++-- | @sepBy1 p sep@ applies /one/ or more occurrences of @p@, separated+-- by @sep@. Returns a list of the values returned by @p@.+--+-- > commaSep p  = p `sepBy1` (char ',')+sepBy1 :: Alternative f => f a -> f s -> f [a]+sepBy1 p s = go+    where go = liftA2 (:) p ((s *> go) <|> pure [])+{-# SPECIALIZE sepBy1 :: Get a -> Get s -> Get [a] #-}++-- | @sepBy1' p sep@ applies /one/ or more occurrences of @p@, separated+-- by @sep@. Returns a list of the values returned by @p@. The value+-- returned by @p@ is forced to WHNF.+--+-- > commaSep p  = p `sepBy1'` (char ',')+sepBy1' :: (MonadPlus m) => m a -> m s -> m [a]+sepBy1' p s = go+    where go = liftM2' (:) p ((s >> go) `mplus` return [])+{-# SPECIALIZE sepBy1' :: Get a -> Get s -> Get [a] #-}++-- | @manyTill p end@ applies action @p@ /zero/ or more times until+-- action @end@ succeeds, and returns the list of values returned by+-- @p@.  This can be used to scan comments:+--+-- >  simpleComment   = string "<!--" *> manyTill anyChar (string "-->")+--+-- (Note the overlapping parsers @anyChar@ and @string \"-->\"@.+-- While this will work, it is not very efficient, as it will cause a+-- lot of backtracking.)+manyTill :: Alternative f => f a -> f b -> f [a]+manyTill p end = go+    where go = (end *> pure []) <|> liftA2 (:) p go+{-# SPECIALIZE manyTill :: Get a -> Get b -> Get [a] #-}++-- | @manyTill' p end@ applies action @p@ /zero/ or more times until+-- action @end@ succeeds, and returns the list of values returned by+-- @p@.  This can be used to scan comments:+--+-- >  simpleComment   = string "<!--" *> manyTill' anyChar (string "-->")+--+-- (Note the overlapping parsers @anyChar@ and @string \"-->\"@.+-- While this will work, it is not very efficient, as it will cause a+-- lot of backtracking.)+--+-- The value returned by @p@ is forced to WHNF.+manyTill' :: (MonadPlus m) => m a -> m b -> m [a]+manyTill' p end = go+    where go = (end >> return []) `mplus` liftM2' (:) p go+{-# SPECIALIZE manyTill' :: Get a -> Get b -> Get [a] #-}++-- | Skip zero or more instances of an action.+skipMany :: Alternative f => f a -> f ()+skipMany p = go+    where go = (p *> go) <|> pure ()+{-# SPECIALIZE skipMany :: Get a -> Get () #-}++-- | Skip one or more instances of an action.+skipMany1 :: Alternative f => f a -> f ()+skipMany1 p = p *> skipMany p+{-# SPECIALIZE skipMany1 :: Get a -> Get () #-}
+ src/Data/Binary/Parser/Numeric.hs view
@@ -0,0 +1,165 @@+{-# LANGUAGE CPP          #-}+-- |+-- Module      :  Data.Binary.Parser.Numeric+-- Copyright   :  Bryan O'Sullivan 2007-2015, Winterland 2016+-- License     :  BSD3+--+-- Maintainer  :  Anton Antich <anton.antich@gmail.com>+-- Stability   :  experimental+-- Portability :  unknown+--+-- Simple, efficient combinator parsing for numeric values.+--+module Data.Binary.Parser.Numeric where++import           Control.Applicative+import           Control.Monad+import           Data.Binary.Get.Internal+import qualified Data.Binary.Parser.Word8     as W+import qualified Data.ByteString              as B+import qualified Data.ByteString.Lex.Integral as LexInt+import           Data.Int+import           Data.Scientific              (Scientific (..))+import qualified Data.Scientific              as Sci+import           Data.Word++#define  MINUS    45+#define  PLUS     43+#define  LITTLE_E 101+#define  BIG_E    69+#define  DOT      46++-- | Parse and decode an unsigned hexadecimal number.  The hex digits+-- @\'a\'@ through @\'f\'@ may be upper or lower case.+--+-- This parser does not accept a leading @\"0x\"@ string.+--+hexadecimal :: (Integral a) => Get a+hexadecimal = do+    bs <- W.takeWhile1 W.isHexDigit+    case LexInt.readHexadecimal bs of+        Just (x, _) -> return x+        Nothing -> fail "hexadecimal: impossible"+{-# SPECIALISE hexadecimal :: Get Int #-}+{-# SPECIALISE hexadecimal :: Get Int8 #-}+{-# SPECIALISE hexadecimal :: Get Int16 #-}+{-# SPECIALISE hexadecimal :: Get Int32 #-}+{-# SPECIALISE hexadecimal :: Get Int64 #-}+{-# SPECIALISE hexadecimal :: Get Integer #-}+{-# SPECIALISE hexadecimal :: Get Word #-}+{-# SPECIALISE hexadecimal :: Get Word8 #-}+{-# SPECIALISE hexadecimal :: Get Word16 #-}+{-# SPECIALISE hexadecimal :: Get Word32 #-}+{-# SPECIALISE hexadecimal :: Get Word64 #-}++-- | Parse and decode an unsigned decimal number.+--+decimal :: Integral a => Get a+decimal = do+    bs <- W.takeWhile1 W.isDigit+    return $! LexInt.readDecimal_ bs+{-# SPECIALISE decimal :: Get Int #-}+{-# SPECIALISE decimal :: Get Int8 #-}+{-# SPECIALISE decimal :: Get Int16 #-}+{-# SPECIALISE decimal :: Get Int32 #-}+{-# SPECIALISE decimal :: Get Int64 #-}+{-# SPECIALISE decimal :: Get Integer #-}+{-# SPECIALISE decimal :: Get Word #-}+{-# SPECIALISE decimal :: Get Word8 #-}+{-# SPECIALISE decimal :: Get Word16 #-}+{-# SPECIALISE decimal :: Get Word32 #-}+{-# SPECIALISE decimal :: Get Word64 #-}++-- | Parse a number with an optional leading @\'+\'@ or @\'-\'@ sign+-- character.+--+signed :: Num a => Get a -> Get a+signed p = do+    w <- W.peek+    if w == MINUS+        then W.skipN 1 >> negate <$> p+        else if w == PLUS then W.skipN 1 >> p else p+{-# SPECIALISE signed :: Get Int -> Get Int #-}+{-# SPECIALISE signed :: Get Int8 -> Get Int8 #-}+{-# SPECIALISE signed :: Get Int16 -> Get Int16 #-}+{-# SPECIALISE signed :: Get Int32 -> Get Int32 #-}+{-# SPECIALISE signed :: Get Int64 -> Get Int64 #-}+{-# SPECIALISE signed :: Get Integer -> Get Integer #-}++-- | Parse a rational number.+--+-- The syntax accepted by this parser is the same as for 'double'.+--+-- /Note/: this parser is not safe for use with inputs from untrusted+-- sources.  An input with a suitably large exponent such as+-- @"1e1000000000"@ will cause a huge 'Integer' to be allocated,+-- resulting in what is effectively a denial-of-service attack.+--+-- In most cases, it is better to use 'double' or 'scientific'+-- instead.+--+rational :: Fractional a => Get a+rational = scientifically realToFrac+{-# SPECIALIZE rational :: Get Double #-}+{-# SPECIALIZE rational :: Get Float #-}+{-# SPECIALIZE rational :: Get Rational #-}+{-# SPECIALIZE rational :: Get Scientific #-}++-- | Parse a rational number and round to 'Double'.+--+-- This parser accepts an optional leading sign character, followed by+-- at least one decimal digit.  The syntax similar to that accepted by+-- the 'read' function, with the exception that a trailing @\'.\'@ or+-- @\'e\'@ /not/ followed by a number is not consumed.+--+-- Examples with behaviour identical to 'read':+--+-- >parseOnly double "3"     == Right ("",1,3.0)+-- >parseOnly double "3.1"   == Right ("",3,3.1)+-- >parseOnly double "3e4"   == Right ("",3,30000.0)+-- >parseOnly double "3.1e4" == Right ("",5,31000.0)+--+-- >parseOnly double ".3"    == Left (".3",0,"takeWhile1")+-- >parseOnly double "e3"    == Left ("e3",0,"takeWhile1")+--+-- Examples of differences from 'read':+--+-- >parseOnly double "3.foo" == Right (".foo",1,3.0)+-- >parseOnly double "3e"    == Right ("e",1,3.0)+--+-- This function does not accept string representations of \"NaN\" or+-- \"Infinity\".+--+double :: Get Double+double = scientifically Sci.toRealFloat++-- | Parse a scientific number.+--+-- The syntax accepted by this parser is the same as for 'double'.+--+scientific :: Get Scientific+scientific = scientifically id++-- | Parse a scientific number and convert to result using a user supply function.+--+-- The syntax accepted by this parser is the same as for 'double'.+--+scientifically :: (Scientific -> a) -> Get a+scientifically h = do+    sign <- W.peek+    when (sign == PLUS || sign == MINUS) (W.skipN 1)+    intPart <- decimal+    sci <- (do fracDigits <- W.word8 DOT >> W.takeWhile1 W.isDigit+               let e' = B.length fracDigits+                   intPart' = intPart * (10 ^ e')+                   fracPart = LexInt.readDecimal_ fracDigits+               parseE (intPart' + fracPart) e'+           ) <|> (parseE intPart 0)++    if sign /= MINUS then return $! h sci else return $! h (negate sci)+  where+    parseE c e =+        (do _ <- W.satisfy (\w -> w ==  LITTLE_E || w == BIG_E)+            (Sci.scientific c . (subtract e) <$> signed decimal)) <|> return (Sci.scientific c (negate e))+    {-# INLINE parseE #-}+{-# INLINE scientifically #-}
+ src/Data/Binary/Parser/Word8.hs view
@@ -0,0 +1,314 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP          #-}+-- |+-- Module      :  Data.Binary.Parser.Word8+-- Copyright   :  Bryan O'Sullivan 2007-2015, Winterland 2016+-- License     :  BSD3+--+-- Maintainer  :  Anton Antich <anton.antich@gmail.com>+-- Stability   :  experimental+-- Portability :  unknown+--+-- Simple, efficient combinator parsing for 'B.ByteString' strings.+--+module Data.Binary.Parser.Word8 where++import           Control.Applicative+import           Control.Monad+import           Data.Binary.Get+import           Data.Binary.Get.Internal+import           Data.ByteString          (ByteString)+import qualified Data.ByteString          as B+import           Data.ByteString.Internal (ByteString (..))+import qualified Data.ByteString.Unsafe   as B+import           Data.Word+import           Foreign.ForeignPtr       (withForeignPtr)+import           Foreign.Ptr              (minusPtr, plusPtr)+import qualified Foreign.Storable         as Storable (Storable (peek))+import           Prelude                  hiding (takeWhile)++#if MIN_VERSION_bytestring(0,10,6)+import           Data.ByteString.Internal (accursedUnutterablePerformIO)+#else+import           Data.ByteString.Internal (inlinePerformIO)++{-# INLINE accursedUnutterablePerformIO #-}+-- | You must be truly desperate to come to me for help.+accursedUnutterablePerformIO :: IO a -> a+accursedUnutterablePerformIO = inlinePerformIO+#endif++--------------------------------------------------------------------------------++-- | Match any byte, to perform lookahead. Returns 'Nothing' if end of+-- input has been reached. Does not consume any input.+--+peekMaybe :: Get (Maybe Word8)+peekMaybe = do+    e <- isEmpty+    if e then return Nothing+         else Just <$> peek+{-# INLINE peekMaybe #-}++-- | Match any byte, to perform lookahead.  Does not consume any+-- input, but will fail if end of input has been reached.+--+peek :: Get Word8+peek = do+    ensureN 1+    bs <- get+    return (B.unsafeHead bs)+{-# INLINE peek #-}++-- | The parser @satisfy p@ succeeds for any byte for which the+-- predicate @p@ returns 'True'. Returns the byte that is actually+-- parsed.+--+-- >digit = satisfy isDigit+-- >    where isDigit w = w >= 48 && w <= 57+--+satisfy :: (Word8 -> Bool) -> Get Word8+satisfy p = do+    ensureN 1+    bs <- get+    let w = B.unsafeHead bs+    if p w then put (B.unsafeTail bs) >> return w+           else fail "satisfy"+{-# INLINE satisfy #-}++-- | The parser @satisfyWith f p@ transforms a byte, and succeeds if+-- the predicate @p@ returns 'True' on the transformed value. The+-- parser returns the transformed byte that was parsed.+--+satisfyWith :: (Word8 -> a) -> (a -> Bool) -> Get a+satisfyWith f p = do+    ensureN 1+    bs <- get+    let w = B.unsafeHead bs+        r = f w+    if p r then put (B.unsafeTail bs) >> return r+           else fail "satisfyWith"+{-# INLINE satisfyWith #-}++-- | Match a specific byte.+--+word8 :: Word8 -> Get ()+word8 c = do+    ensureN 1+    bs <- get+    let w = B.unsafeHead bs+    if c == w then put (B.unsafeTail bs)+              else fail "word8"+{-# INLINE word8 #-}++-- | Match any byte.+--+anyWord8 :: Get Word8+anyWord8 = getWord8+{-# INLINE anyWord8 #-}++-- | The parser @skipWord8 p@ succeeds for any byte for which the predicate @p@ returns 'True'.+--+skipWord8 :: (Word8 -> Bool) -> Get ()+skipWord8 p = do+    ensureN 1+    bs <- get+    let w = B.unsafeHead bs+    if p w then put (B.unsafeTail bs)+              else fail "skip"+{-# INLINE skipWord8 #-}++--------------------------------------------------------------------------------++-- | This is a faster version of 'skip' for small N (smaller than chunk size).+--+skipN :: Int -> Get ()+skipN n = do+    bs <- get+    let l = B.length bs+    if l > n then put (B.unsafeDrop n bs)+             else skip n+{-# INLINE skipN #-}++-- | Consume input as long as the predicate returns 'False' or reach the end of input,+-- and return the consumed input.+--+takeTill :: (Word8 -> Bool) -> Get ByteString+takeTill p = do+    bs <- get+    let (want, rest) = B.break p bs+    put rest+    if B.null rest then B.concat . reverse <$> go [want] else return want+  where+    go acc = do+        e <- isEmpty -- isEmpty will draw input here+        if e+        then return acc+        else do+            bs <- get+            let (want, rest) = B.break p bs+                acc' = want : acc+            put rest+            if B.null rest then go acc' else return acc'+{-# INLINE takeTill #-}++-- | Consume input as long as the predicate returns 'True' or reach the end of input,+-- and return the consumed input.+--+takeWhile :: (Word8 -> Bool) -> Get ByteString+takeWhile p = do+    bs <- get+    let (want, rest) = B.span p bs+    put rest+    if B.null rest then B.concat . reverse <$> go [want] else return want+  where+    go acc = do+        e <- isEmpty+        if e+        then return acc+        else do+            bs <- get+            let (want, rest) = B.span p bs+                acc' = want : acc+            put rest+            if B.null rest then go acc' else return acc'+{-# INLINE takeWhile #-}++-- | Similar to 'takeWhile', but requires the predicate to succeed on at least one byte+-- of input: it will fail if the predicate never returns 'True' or reach the end of input+--+takeWhile1 :: (Word8 -> Bool) -> Get ByteString+takeWhile1 p = do+    bs <- takeWhile p+    if B.null bs then fail "takeWhile1" else return bs+{-# INLINE takeWhile1 #-}++-- | Skip past input for as long as the predicate returns 'True'.+--+skipWhile :: (Word8 -> Bool) -> Get ()+skipWhile p = do+    bs <- get+    let rest = B.dropWhile p bs+    put rest+    when (B.null rest) go+  where+    go = do+        e <- isEmpty+        unless e $ do+            bs <- get+            let rest = B.dropWhile p bs+            put rest+            when (B.null rest) go+{-# INLINE skipWhile #-}++-- | Skip over white space using 'isSpace'.+--+skipSpaces :: Get ()+skipSpaces = skipWhile isSpace+{-# INLINE skipSpaces #-}++-- | @string s@ parses a sequence of bytes that identically match @s@.+--+string :: ByteString -> Get ()+string bs = do+    let l = B.length bs+    bs' <- get+    if l <= B.length bs'              -- current chunk is enough+    then if B.unsafeTake l bs' == bs+        then put (B.unsafeDrop l bs')+        else fail "string"+    else do+        ensureN l+        bs'' <- get+        if B.unsafeTake l bs'' == bs+        then put (B.unsafeDrop l bs'')+        else fail "string"+{-# INLINE string #-}++-- | A stateful scanner.  The predicate consumes and transforms a+-- state argument, and each transformed state is passed to successive+-- invocations of the predicate on each byte of the input until one+-- returns 'Nothing' or the input ends.+--+-- This parser does not fail.  It will return an empty string if the+-- predicate returns 'Nothing' on the first byte of input.+--+scan :: s -> (s -> Word8 -> Maybe s) -> Get ByteString+scan s0 consume = withInputChunks s0 consume' B.concat (return . B.concat)+  where+    consume' s1 (PS fp off len) = accursedUnutterablePerformIO $+        withForeignPtr fp $ \ptr0 -> do+            let start = ptr0 `plusPtr` off+                end   = start `plusPtr` len+            go fp off start end start s1+    go fp off start end ptr !s+        | ptr < end = do+            w <- Storable.peek ptr+            case consume s w of+                Just s' -> go fp off start end (ptr `plusPtr` 1) s'+                _       -> do+                    let !len1 = ptr `minusPtr` start+                        !off2 = off + len1+                        !len2 = end `minusPtr` ptr+                    return (Right (PS fp off len1, PS fp off2 len2))+        | otherwise = return (Left s)+{-# INLINE scan #-}++-- | Similar to 'scan', but working on 'ByteString' chunks, The predicate+-- consumes a 'ByteString' chunk and transforms a state argument,+-- and each transformed state is passed to successive invocations of+-- the predicate on each chunk of the input until one chunk got splited to+-- @Right (ByteString, ByteString)@ or the input ends.+--+scanChunks :: s -> Consume s -> Get ByteString+scanChunks s consume = withInputChunks s consume B.concat (return . B.concat)+{-# INLINE scanChunks #-}++--------------------------------------------------------------------------------++-- | Fast 'Word8' predicate for matching ASCII space characters+--+-- >isSpace w = w == 32 || w - 9 <= 4+--+isSpace :: Word8 -> Bool+isSpace w = w == 32 || w - 9 <= 4+{-# INLINE isSpace #-}++-- | Decimal digit predicate.+--+isDigit :: Word8 -> Bool+isDigit w = w - 48 <= 9+{-# INLINE isDigit #-}++-- | Hex digit predicate.+--+isHexDigit :: Word8 -> Bool+isHexDigit w = (w >= 48 && w <= 57) || (w >= 97 && w <= 102) || (w >= 65 && w <= 70)+{-# INLINE isHexDigit #-}++-- | A predicate that matches either a space @\' \'@ or horizontal tab+-- @\'\\t\'@ character.+--+isHorizontalSpace :: Word8 -> Bool+isHorizontalSpace w = w == 32 || w == 9+{-# INLINE isHorizontalSpace #-}++-- | A predicate that matches either a carriage return @\'\\r\'@ or+-- newline @\'\\n\'@ character.+--+isEndOfLine :: Word8 -> Bool+isEndOfLine w = w == 13 || w == 10+{-# INLINE isEndOfLine #-}++--------------------------------------------------------------------------------++-- | Match either a single newline byte @\'\\n\'@, or a carriage+-- return followed by a newline byte @\"\\r\\n\"@.+endOfLine :: Get ()+endOfLine = do+    w <- getWord8+    case w of+        10 -> return ()+        13 -> word8 10+        _  -> fail "endOfLine"+{-# INLINE endOfLine #-}
+ src/Data/Connection.hs view
@@ -0,0 +1,25 @@+module Data.Connection where++import qualified Data.ByteString           as B+import qualified Data.ByteString.Lazy.Internal as L+import qualified System.IO.Streams         as S++-- | Low-level byte-stream transport. Application MariaDB code should use+-- the managed connection in "Database.MySQL.Base", which adds command leases,+-- framing, cancellation retirement and bounded cleanup.+--+-- The source supports io-streams pushback; reads may block until data/EOF.+-- Sending accepts lazy bytes so builders can supply chunks without flattening.+-- 'connExtraInfo' carries transport-specific metadata such as a socket address.+--+-- The caller owns this raw resource: bracket 'close', coordinate outstanding+-- reads/writes before closing, and never use the connection after close.+-- A peer disconnect or operation exception is not itself a generic guarantee+-- that every implementation of this record released its underlying resource.+--+data Connection a = Connection+    { source        :: {-# UNPACK #-} !(S.InputStream B.ByteString)   -- ^ receive data as 'S.InputStream'+    , send          :: L.ByteString -> IO ()                          -- ^ send data with connection+    , close         :: IO ()                                          -- ^ close connection+    , connExtraInfo :: a                                              -- ^ extra info+    }
+ src/Data/Int/Int24.hs view
@@ -0,0 +1,250 @@+{-# OPTIONS_GHC -Wno-inline-rule-shadowing #-}++{-# LANGUAGE BangPatterns      #-}+{-# LANGUAGE CPP               #-}+{-# LANGUAGE MagicHash         #-}+{-# LANGUAGE NoImplicitPrelude #-}++#if !MIN_VERSION_base(4,8,0)+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE StandaloneDeriving #-}+#endif++-----------------------------------------------------------------------------+-- |+-- Module      :  Data.Int.Int24+-- Copyright   :  (c) The University of Glasgow 1997-2002+-- License     :  see src/Data/LICENSE+--+-- Stability   :  experimental+-- Portability :  non-portable (GHC Extensions)+--+-- The 24 bit integral datatype, 'Int24'.+--+-----------------------------------------------------------------------------++module Data.Int.Int24 (+  -- * Int24 type+  Int24(..)+  -- * Internal helper+  , narrow24Int#+  ) where++import           Data.Bits+import           Data.Data+import           Data.Maybe+import           Data.Word.Word24+import           Foreign.Storable++import           GHC.Arr+import           GHC.Base+import           GHC.Enum+import           GHC.Int.Compat+import           GHC.Integer (smallInteger, integerToInt)+import           GHC.Num hiding (integerToInt)+import           GHC.Ptr+import           GHC.Read+import           GHC.Real+import           GHC.Show+import           GHC.Word.Compat++import           Control.DeepSeq++#if !MIN_VERSION_base(4,8,0)+import           Data.Typeable+#endif++------------------------------------------------------------------------++-- Int24 is represented in the same way as Int. Operations may assume+-- and must ensure that it holds only values from its logical range.++-- | 24-bit signed integer type+data Int24 = I24# Int# deriving (Eq, Ord)++#if !MIN_VERSION_base(4,8,0)+deriving instance Typeable Int24+#endif++instance NFData Int24  where rnf !_ = ()++int24Type :: DataType+int24Type = mkIntType "Data.Word.Int24.Int24"++instance Data Int24 where+  toConstr x = mkIntegralConstr int24Type x+  gunfold _ z c = case constrRep c of+                    (IntConstr x) -> z (fromIntegral x)+                    _ -> error $ "Data.Data.gunfold: Constructor " ++ show c+                                 ++ " is not of type Int24."+  dataTypeOf _ = int24Type++-- the narrowings are primops in GHC; I don't have that luxury.+-- if the 24th bit (from right) is on, the value is negative, so+-- fill the uppermost bits with 1s.  Otherwise clear them to 0s.+narrow24Int# :: Int# -> Int#+narrow24Int# x# = if isTrue# ((x'# `and#` mask#) `eqWord#` mask#)+    then word2Int# (x'# `or#`  int2Word# m1#)+    else word2Int# (x'# `and#` int2Word# m2#)+  where+    !x'#   = int2Word# x#+    !mask# = int2Word# 0x00800000#+    !(I# m1#) = -8388608+    !(I# m2#) = 16777215++instance Show Int24 where+    showsPrec p x = showsPrec p (fromIntegral x :: Int)++instance Num Int24 where+    (I24# x#) + (I24# y#)  = I24# (narrow24Int# (x# +# y#))+    (I24# x#) - (I24# y#)  = I24# (narrow24Int# (x# -# y#))+    (I24# x#) * (I24# y#)  = I24# (narrow24Int# (x# *# y#))+    negate (I24# x#)       = I24# (narrow24Int# (negateInt# x#))+    abs x | x >= 0         = x+          | otherwise      = negate x+    signum x | x > 0       = 1+    signum 0               = 0+    signum _               = -1+    fromInteger i          = I24# (narrow24Int# (integerToInt i))++instance Real Int24 where+    toRational x = toInteger x % 1++instance Enum Int24 where+    succ x+        | x /= maxBound = x + 1+        | otherwise     = succError "Int24"+    pred x+        | x /= minBound = x - 1+        | otherwise     = predError "Int24"+    toEnum i@(I# i#)+        | i >= fromIntegral (minBound::Int24) && i <= fromIntegral (maxBound::Int24)+                        = I24# i#+        | otherwise     = toEnumError "Int24" i (minBound::Int24, maxBound::Int24)+    fromEnum (I24# x#)  = I# x#+    enumFrom            = boundedEnumFrom+    enumFromThen        = boundedEnumFromThen++instance Integral Int24 where+    quot    x@(I24# x#) y@(I24# y#)+        | y == 0                     = divZeroError+        | x == minBound && y == (-1) = overflowError+        | otherwise                  = I24# (narrow24Int# (x# `quotInt#` y#))+    rem     x@(I24# x#) y@(I24# y#)+        | y == 0                     = divZeroError+        | x == minBound && y == (-1) = overflowError+        | otherwise                  = I24# (narrow24Int# (x# `remInt#` y#))+    div     x@(I24# x#) y@(I24# y#)+        | y == 0                     = divZeroError+        | x == minBound && y == (-1) = overflowError+        | otherwise                  = I24# (narrow24Int# (x# `divInt#` y#))+    mod     x@(I24# x#) y@(I24# y#)+        | y == 0                     = divZeroError+        | x == minBound && y == (-1) = overflowError+        | otherwise                  = I24# (narrow24Int# (x# `modInt#` y#))+    quotRem x@(I24# x#) y@(I24# y#)+        | y == 0                     = divZeroError+        | x == minBound && y == (-1) = overflowError+        | otherwise                  = (I24# (narrow24Int# (x# `quotInt#` y#)),+                                        I24# (narrow24Int# (x# `remInt#` y#)))+    divMod  x@(I24# x#) y@(I24# y#)+        | y == 0                     = divZeroError+        | x == minBound && y == (-1) = overflowError+        | otherwise                  = (I24# (narrow24Int# (x# `divInt#` y#)),+                                        I24# (narrow24Int# (x# `modInt#` y#)))+    toInteger (I24# x#)              = smallInteger x#++instance Bounded Int24 where+    minBound = -0x800000+    maxBound =  0x7FFFFF++instance Ix Int24 where+    range (m,n)         = [m..n]+    unsafeIndex (m,_) i = fromIntegral i - fromIntegral m+    inRange (m,n) i     = m <= i && i <= n++instance Read Int24 where+    readsPrec p s = [(fromIntegral (x::Int), r) | (x, r) <- readsPrec p s]++instance Bits Int24 where+    {-# INLINE shift #-}+    {-# INLINE bit #-}+    {-# INLINE testBit #-}++    (I24# x#) .&.   (I24# y#)  = I24# (word2Int# (int2Word# x# `and#` int2Word# y#))+    (I24# x#) .|.   (I24# y#)  = I24# (word2Int# (int2Word# x# `or#`  int2Word# y#))+    (I24# x#) `xor` (I24# y#)  = I24# (word2Int# (int2Word# x# `xor#` int2Word# y#))+    complement (I24# x#)       = I24# (word2Int# (not# (int2Word# x#)))+    (I24# x#) `shift` (I# i#)+        | isTrue# (i# >=# 0#)  = I24# (narrow24Int# (x# `iShiftL#` i#))+        | otherwise            = I24# (x# `iShiftRA#` negateInt# i#)+    (I24# x#) `shiftL`       (I# i#) = I24# (narrow24Int# (x# `iShiftL#` i#))+    (I24# x#) `unsafeShiftL` (I# i#) = I24# (narrow24Int# (x# `uncheckedIShiftL#` i#))+    (I24# x#) `shiftR`       (I# i#) = I24# (x# `iShiftRA#` i#)+    (I24# x#) `unsafeShiftR` (I# i#) = I24# (x# `uncheckedIShiftRA#` i#)+    (I24# x#) `rotate` i+        | isTrue# (i'# ==# 0#) = I24# x#+        | otherwise = I24# (narrow24Int# (word2Int# ((x'# `uncheckedShiftL#` i'#) `or#`+                                         (x'# `uncheckedShiftRL#` (24# -# i'#)))))+      where+        !x'# = narrow24Word# (int2Word# x#)+        !(I# i'#) = i `mod` 24+    bitSizeMaybe i             = Just (finiteBitSize i)+    bitSize                    = finiteBitSize+    isSigned _                 = True+    popCount (I24# x#)         = I# (word2Int# (popCnt24# (int2Word# x#)))+    bit                        = bitDefault+    testBit                    = testBitDefault++instance FiniteBits Int24 where+    finiteBitSize _ = 24+#if MIN_VERSION_base(4,8,0)+    countLeadingZeros  (I24# x#) = I# (word2Int# (clz24# (int2Word# x#)))+    countTrailingZeros (I24# x#) = I# (word2Int# (ctz24# (int2Word# x#)))+#endif++{-# RULES+"fromIntegral/Word8->Int24"   fromIntegral = \(W8# x#) -> I24# (word2Int# x#)+"fromIntegral/Word16->Int24"  fromIntegral = \(W16# x#) -> I24# (word2Int# x#)+"fromIntegral/Int8->Int24"    fromIntegral = \(I8# x#) -> I24# x#+"fromIntegral/Int16->Int24"   fromIntegral = \(I16# x#) -> I24# x#+"fromIntegral/Int24->Int24"   fromIntegral = id :: Int24 -> Int24+"fromIntegral/a->Int24"       fromIntegral = \x -> case fromIntegral x of I# x# -> I24# (narrow24Int# x#)+"fromIntegral/Int24->a"       fromIntegral = \(I24# x#) -> fromIntegral (I# x#)+  #-}++{-# RULES+"properFraction/Float->(Int24,Float)"+    properFraction = \x ->+                      case properFraction x of {+                        (n, y) -> ((fromIntegral :: Int -> Int24) n, y :: Float) }+"truncate/Float->Int24"+    truncate = (fromIntegral :: Int -> Int24) . (truncate :: Float -> Int)+"floor/Float->Int24"+    floor    = (fromIntegral :: Int -> Int24) . (floor :: Float -> Int)+"ceiling/Float->Int24"+    ceiling  = (fromIntegral :: Int -> Int24) . (ceiling :: Float -> Int)+"round/Float->Int24"+    round    = (fromIntegral :: Int -> Int24) . (round  :: Float -> Int)+  #-}++{-# RULES+"properFraction/Double->(Int24,Double)"+    properFraction = \x ->+                      case properFraction x of {+                        (n, y) -> ((fromIntegral :: Int -> Int24) n, y :: Double) }+"truncate/Double->Int24"+    truncate = (fromIntegral :: Int -> Int24) . (truncate :: Double -> Int)+"floor/Double->Int24"+    floor    = (fromIntegral :: Int -> Int24) . (floor :: Double -> Int)+"ceiling/Double->Int24"+    ceiling  = (fromIntegral :: Int -> Int24) . (ceiling :: Double -> Int)+"round/Double->Int24"+    round    = (fromIntegral :: Int -> Int24) . (round  :: Double -> Int)+  #-}++instance Storable Int24 where+  sizeOf _ = 3+  alignment _ = 3+  peek p = fmap fromIntegral $ peek ((castPtr p) :: Ptr Word24)+  poke p v = poke (castPtr p :: Ptr Word24) (fromIntegral v)
+ src/Data/TLSSetting.hs view
@@ -0,0 +1,173 @@+{-# LANGUAGE CPP #-}++-- | Construct verified TLS parameters for MariaDB connections. Choose managed+-- OS roots or an application CA file and reuse parameters across connections.+-- The connection constructor supplies the certificate hostname/default SNI.+-- Customize cipher/credential policy through the @tls@ package when needed.+--+-- File/PEM/credential errors throw during parameter construction. These helpers+-- do not disable peer verification or make a plaintext connection secure.+--+module Data.TLSSetting+    ( -- * Choose a CAStore+      TrustedCAStore(..)+      -- * Make TLS settings+    , makeClientParams+    , makeClientParams'+    , makeServerParams+    , makeServerParams'+      -- * Internal+    , mozillaCAStorePath+    ) where++import           Control.Monad (when)+import qualified Data.ByteString            as B+import qualified Data.PEM                   as X509+import qualified Data.X509                  as X509+import qualified Data.X509.CertificateStore as X509+import qualified Network.TLS                as TLS+import qualified Network.TLS.Extra          as TLS+import           Paths_mariadb_haskell          (getDataFileName)+import qualified System.X509                as X509++#if !MIN_VERSION_tls(2,1,4)+import           Data.Default.Class         (def)+#endif++-- | Trust anchors used by the TLS verifier alongside hostname and certificate+-- checks. Prefer an OS/application-managed store with an explicit update policy.+data TrustedCAStore+    = SystemCAStore                   -- ^ Read the operating system's trust roots.+    | MozillaCAStore                  -- ^ Installed Mozilla snapshot; see docs/CA_BUNDLE.md for source, checksum and update policy.+    | CustomCAStore FilePath          -- ^ PEM CA file containing one or more certificates; malformed/empty files fail.+  deriving (Show, Eq)++-- | Resolve the installed bundle through Cabal's data directory, independent+-- of the working directory. This does not refresh or validate its contents.+mozillaCAStorePath :: IO FilePath+mozillaCAStorePath = getDataFileName "mozillaCAStore.pem"++makeCAStore :: TrustedCAStore -> IO X509.CertificateStore+makeCAStore SystemCAStore       = X509.getSystemCertificateStore+makeCAStore MozillaCAStore      = makeCAStore . CustomCAStore =<< mozillaCAStorePath+makeCAStore (CustomCAStore fp)  = do+    bs <- B.readFile fp+    pems <- either (ioError . userError) pure (X509.pemParseBS bs)+    when (null pems) (ioError (userError "CA file contains no certificates"))+    case mapM (X509.decodeSignedCertificate . X509.pemContent) pems of+        Right cas -> return (X509.makeCertificateStore cas)+        Left err  -> ioError (userError err)++-- | Verify server certificates using the selected roots, without supplying a+-- client certificate. The connection constructor later supplies the server+-- identity; pass its certificate DNS name rather than disabling verification.+-- Root loading happens here, so reuse the result in a connection factory.+--+makeClientParams :: TrustedCAStore          -- ^ trusted certificates.+                 -> IO TLS.ClientParams+makeClientParams tca = do+    caStore <- makeCAStore tca+    return (TLS.defaultParamsClient "" B.empty)+        {   TLS.clientSupported = defaultSupported' { TLS.supportedCiphers = TLS.ciphersuite_default }+        ,   TLS.clientShared    = defaultShared'+            {   TLS.sharedCAStore         = caStore+            ,   TLS.sharedValidationCache = defaultValidationCache'+            }+        }++-- | Verify the server and supply a client certificate/key for mutual TLS.+--+-- The helper installs the sole supplied credential in the certificate-request+-- hook; sharedCredentials alone does not answer a server's client-cert request.+-- Server identity is still verified against the supplied CA store.+--+makeClientParams' :: FilePath       -- ^ public certificate (X.509 format).+                  -> [FilePath]     -- ^ chain certificates (X.509 format).+                                    --   the root of your certificate chain should be+                                    --   already trusted by server, or tls will fail.+                  -> FilePath       -- ^ private key associated.+                  -> TrustedCAStore -- ^ trusted certificates.+                  -> IO TLS.ClientParams+makeClientParams' pub certs priv tca = do+    p <- makeClientParams tca+    c <- TLS.credentialLoadX509Chain pub certs priv+    case c of+        Right c' ->+            return p+                {   TLS.clientHooks = (TLS.clientHooks p)+                    { TLS.onCertificateRequest = \_ -> pure (Just c') }+                ,   TLS.clientShared = (TLS.clientShared p)+                    {+                        TLS.sharedCredentials = TLS.Credentials [c']+                    }+                }+        Left err -> ioError (userError err)++-- | Low-level TLS server helper retained for compatibility and transport tests.+-- Supplies a server credential without requesting a client certificate; it does+-- not implement a MariaDB server. Ordinary client applications use 'makeClientParams'.+--+makeServerParams :: FilePath        -- ^ public certificate (X.509 format).+                 -> [FilePath]      -- ^ chain certificates (X.509 format).+                                    --   the root of your certificate chain should be+                                    --   already trusted by client, or tls will fail.+                 -> FilePath        -- ^ private key associated.+                 -> IO TLS.ServerParams+makeServerParams pub certs priv = do+    c <- TLS.credentialLoadX509Chain pub certs priv+    case c of+        Right c'@(X509.CertificateChain c'', _) ->+            return defaultServerParams'+                {   TLS.serverCACertificates =  c''+                ,   TLS.serverShared = defaultShared'+                    {+                        TLS.sharedCredentials = TLS.Credentials [c']+                    }+                ,   TLS.serverSupported = defaultSupported' { TLS.supportedCiphers = TLS.ciphersuite_strong }+                }+        Left err -> ioError (userError err)++-- | Low-level TLS server helper requesting and verifying client certificates+-- against the selected roots. Applications still enforce authorization separately.+--+makeServerParams' :: FilePath       -- ^ public certificate (X.509 format).+                  -> [FilePath]     -- ^ chain certificates (X.509 format).+                  -> FilePath       -- ^ private key associated.+                  -> TrustedCAStore -- ^ server will use these certificates to validate clients.+                  -> IO TLS.ServerParams+makeServerParams' pub certs priv tca = do+    caStore <- makeCAStore tca+    p <- makeServerParams pub certs priv+    return p+        {   TLS.serverWantClientCert = True+        ,   TLS.serverShared = (TLS.serverShared p)+            {   TLS.sharedCAStore = caStore+            }+        }++-- Compatibility shims for tls < 2.1.4 vs >= 2.1.4+#if MIN_VERSION_tls(2,1,4)+defaultServerParams' :: TLS.ServerParams+defaultServerParams' = TLS.defaultParamsServer++defaultShared' :: TLS.Shared+defaultShared' = TLS.defaultShared++defaultSupported' :: TLS.Supported+defaultSupported' = TLS.defaultSupported++defaultValidationCache' :: TLS.ValidationCache+defaultValidationCache' = TLS.defaultValidationCache+#else+defaultServerParams' :: TLS.ServerParams+defaultServerParams' = def++defaultShared' :: TLS.Shared+defaultShared' = def++defaultSupported' :: TLS.Supported+defaultSupported' = def++defaultValidationCache' :: TLS.ValidationCache+defaultValidationCache' = def+#endif
+ src/Data/Word/Word24.hs view
@@ -0,0 +1,285 @@+{-# OPTIONS_GHC -Wno-inline-rule-shadowing #-}++{-# LANGUAGE BangPatterns      #-}+{-# LANGUAGE CPP               #-}+{-# LANGUAGE MagicHash         #-}+{-# LANGUAGE NoImplicitPrelude #-}++#if !MIN_VERSION_base(4,8,0)+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE StandaloneDeriving #-}+#endif++-- |+-- Module      : Data.Word.Word24+-- License     : see  src/Data/LICENSE+-- Stability   : experimental+-- Portability : non-portable (GHC Extensions)++-- Provide a 24-bit unsigned integral type: 'Word24', analagous to Word8,+-- Word16, etc.+--++module Data.Word.Word24 (+  -- * Word24 type+    Word24(..)+  , byteSwap24+  , byteSwap24#+  -- * Internal helpers+  , narrow24Word#+#if MIN_VERSION_base(4,8,0)+  , clz24#+  , ctz24#+#endif+  , popCnt24#+  )++where++import           Data.Bits+import           Data.Data+import           Data.Maybe+import           Foreign.Storable++import           GHC.Arr+import           GHC.Base+import           GHC.Enum+import           GHC.Num hiding (integerToWord)+import           GHC.Ptr+import           GHC.Read+import           GHC.Real+import           GHC.Show+import           GHC.Word.Compat+import           GHC.Integer (smallInteger, integerToWord)++import           Control.DeepSeq++#if !MIN_VERSION_base(4,8,0)+import           Data.Typeable+#endif++------------------------------------------------------------------------++-- Word24 is represented in the same way as Word.  Operations may assume and+-- must ensure that it holds only values in its logical range.++-- | 24-bit unsigned integer type+--+data Word24 = W24# Word# deriving (Eq, Ord)++#if !MIN_VERSION_base(4,8,0)+deriving instance Typeable Word24+#endif++instance NFData Word24 where rnf !_ = ()++word24Type :: DataType+word24Type = mkIntType "Data.Word.Word24.Word24"++instance Data Word24 where+  toConstr x = mkIntegralConstr word24Type x+  gunfold _ z c = case constrRep c of+                    (IntConstr x) -> z (fromIntegral x)+                    _ -> error $ "Data.Data.gunfold: Constructor " ++ show c+                                 ++ " is not of type Word24."+  dataTypeOf _ = word24Type++-- | narrowings represented as primop 'and#' in GHC.+narrow24Word# :: Word# -> Word#+narrow24Word# = and# 0xFFFFFF##++#if MIN_VERSION_base(4,8,0)+-- | count leading zeros+--+clz24# :: Word# -> Word#+clz24# w# = clz32# (narrow24Word# w#) `minusWord#` 8##++-- | count trailing zeros+--+ctz24# :: Word# -> Word#+ctz24# w# = ctz# (w# `or#` 0x1000000##)+#endif++-- | the number of set bits+--+popCnt24# :: Word# -> Word#+popCnt24# w# = popCnt# (narrow24Word# w#)++instance Show Word24 where+  showsPrec p x = showsPrec p (fromIntegral x :: Int)++instance Num Word24 where+  (W24# x#) + (W24# y#) = W24# (narrow24Word# (x# `plusWord#` y#))+  (W24# x#) - (W24# y#) = W24# (narrow24Word# (x# `minusWord#` y#))+  (W24# x#) * (W24# y#) = W24# (narrow24Word# (x# `timesWord#` y#))+  negate (W24# x#)      = W24# (narrow24Word# (int2Word# (negateInt# (word2Int# x#))))+  abs x                 = x+  signum 0              = 0+  signum _              = 1+  fromInteger i         = W24# (narrow24Word# (integerToWord i))++instance Real Word24 where+  toRational x = toInteger x % 1++instance Enum Word24 where+  succ x+    | x /= maxBound  = x + 1+    | otherwise      = succError "Word24"+  pred x+    | x /= minBound  = x - 1+    | otherwise      = predError "Word24"+  toEnum i@(I# i#)+    | i >= 0 && i <= fromIntegral (maxBound :: Word24)+                     = W24# (int2Word# i#)+    | otherwise      = toEnumError "Word24" i (minBound::Word24, maxBound::Word24)+  fromEnum (W24# x#) = I# (word2Int# x#)+  enumFrom           = boundedEnumFrom+  enumFromThen       = boundedEnumFromThen++instance Integral Word24 where+  quot (W24# x#) y@(W24# y#)+    | y /= 0                 = W24# (x# `quotWord#` y#)+    | otherwise              = divZeroError+  rem (W24# x#) y@(W24# y#)+    | y /= 0                 = W24# (x# `remWord#` y#)+    | otherwise              = divZeroError+  div (W24# x#) y@(W24# y#)+    | y /= 0                 = W24# (x# `quotWord#` y#)+    | otherwise              = divZeroError+  mod (W24# x#) y@(W24# y#)+    | y /= 0                 = W24# (x# `remWord#` y#)+    | otherwise              = divZeroError+  quotRem (W24# x#) y@(W24# y#)+    | y /= 0                 = (W24# (x# `quotWord#` y#), W24# (x# `remWord#` y#))+    | otherwise              = divZeroError+  divMod (W24# x#) y@(W24# y#)+    | y /= 0                 = (W24# (x# `quotWord#` y#), W24# (x# `remWord#` y#))+    | otherwise              = divZeroError+  toInteger (W24# x#)        = smallInteger (word2Int# x#)++instance Bounded Word24 where+  minBound = 0+  maxBound = 0xFFFFFF++instance Ix Word24 where+  range (m,n)         = [m..n]+  unsafeIndex (m,_) i = fromIntegral (i - m)+  inRange (m,n) i     = m <= i && i <= n++instance Read Word24 where+  readsPrec p s = [(fromIntegral (x::Int), r) | (x, r) <- readsPrec p s]++instance Bits Word24 where+    {-# INLINE shift #-}+    {-# INLINE bit #-}+    {-# INLINE testBit #-}++    (W24# x#) .&.   (W24# y#)  = W24# (x# `and#` y#)+    (W24# x#) .|.   (W24# y#)  = W24# (x# `or#`  y#)+    (W24# x#) `xor` (W24# y#)  = W24# (x# `xor#` y#)+    complement (W24# x#)       = W24# (x# `xor#` mb#) where !(W24# mb#) = maxBound+    (W24# x#) `shift` (I# i#)+        | isTrue# (i# >=# 0#)  = W24# (narrow24Word# (x# `shiftL#` i#))+        | otherwise            = W24# (x# `shiftRL#` negateInt# i#)+    (W24# x#) `shiftL` (I# i#)       = W24# (narrow24Word# (x# `shiftL#` i#))+    (W24# x#) `unsafeShiftL` (I# i#) =+        W24# (narrow24Word# (x# `uncheckedShiftL#` i#))+    (W24# x#) `shiftR`       (I# i#) = W24# (x# `shiftRL#` i#)+    (W24# x#) `unsafeShiftR` (I# i#) = W24# (x# `uncheckedShiftRL#` i#)+    (W24# x#) `rotate`       i+        | isTrue# (i'# ==# 0#) = W24# x#+        | otherwise  = W24# (narrow24Word# ((x# `uncheckedShiftL#` i'#) `or#`+                                            (x# `uncheckedShiftRL#` (24# -# i'#))))+      where+        !(I# i'#) = i `mod` 24+    bitSizeMaybe i            = Just (finiteBitSize i)+    bitSize                   = finiteBitSize+    isSigned _                = False+    popCount (W24# x#)        = I# (word2Int# (popCnt24# x#))+    bit                       = bitDefault+    testBit                   = testBitDefault++instance FiniteBits Word24 where+    finiteBitSize _ = 24+#if MIN_VERSION_base(4,8,0)+    countLeadingZeros  (W24# x#) = I# (word2Int# (clz24# x#))+    countTrailingZeros (W24# x#) = I# (word2Int# (ctz24# x#))+#endif++-- | Swap bytes in 'Word24'.+--+byteSwap24 :: Word24 -> Word24+byteSwap24 (W24# w#) = W24# (byteSwap24# w#)++byteSwap24# :: Word# -> Word#+byteSwap24# w# = let byte0 = uncheckedShiftL# (and# w# 0x0000ff##) 16#+                     byte1 = and# w# 0x00ff00##+                     byte2 = uncheckedShiftRL# (and# w# 0xff0000##) 16#+                 in byte0 `or#` byte1 `or#` byte2++{-# RULES+"fromIntegral/Word8->Word24"    fromIntegral = \(W8# x#) -> W24# x#+"fromIntegral/Word16->Word24"   fromIntegral = \(W16# x#) -> W24# x#+"fromIntegral/Word24->Word24"   fromIntegral = id :: Word24 -> Word24+"fromIntegral/Word24->Integer"  fromIntegral = toInteger :: Word24 -> Integer+"fromIntegral/a->Word24"        fromIntegral = \x -> case fromIntegral x of W# x# -> W24# (narrow24Word# x#)+"fromIntegral/Word24->a"        fromIntegral = \(W24# x#) -> fromIntegral (W# x#)+  #-}++{-# RULES+"properFraction/Float->(Word24,Float)"+    properFraction = \x ->+                      case properFraction x of {+                        (n, y) -> ((fromIntegral :: Int -> Word24) n, y :: Float) }+"truncate/Float->Word24"+    truncate = (fromIntegral :: Int -> Word24) . (truncate :: Float -> Int)+"floor/Float->Word24"+    floor    = (fromIntegral :: Int -> Word24) . (floor :: Float -> Int)+"ceiling/Float->Word24"+    ceiling  = (fromIntegral :: Int -> Word24) . (ceiling :: Float -> Int)+"round/Float->Word24"+    round    = (fromIntegral :: Int -> Word24) . (round  :: Float -> Int)+  #-}++{-# RULES+"properFraction/Double->(Word24,Double)"+    properFraction = \x ->+                      case properFraction x of {+                        (n, y) -> ((fromIntegral :: Int -> Word24) n, y :: Double) }+"truncate/Double->Word24"+    truncate = (fromIntegral :: Int -> Word24) . (truncate :: Double -> Int)+"floor/Double->Word24"+    floor    = (fromIntegral :: Int -> Word24) . (floor :: Double -> Int)+"ceiling/Double->Word24"+    ceiling  = (fromIntegral :: Int -> Word24) . (ceiling :: Double -> Int)+"round/Double->Word24"+    round    = (fromIntegral :: Int -> Word24) . (round  :: Double -> Int)+  #-}++readWord24OffPtr :: Ptr Word24 -> IO Word24+readWord24OffPtr p = do+  let p' = castPtr p :: Ptr Word8+  w1 <- peekElemOff p' 0+  w2 <- peekElemOff p' 1+  w3 <- peekElemOff p' 2+  let w1' = (fromIntegral :: (Word8 -> Word24)) w1+      w2' = (fromIntegral :: (Word8 -> Word24)) w2+      w3' = (fromIntegral :: (Word8 -> Word24)) w3+      w = w1' .|. (w2' `shiftL` 8) .|. (w3' `shiftL` 16)+  return w++writeWord24ToPtr :: Ptr Word24 -> Word24 -> IO ()+writeWord24ToPtr p v = do+    let w1 = fromIntegral (v .&. 0x0000FF) :: Word8+        w2 = fromIntegral ((v .&. 0x00FF00) `shiftR` 8) :: Word8+        w3 = fromIntegral ((v .&. 0xFF0000) `shiftR` 16) :: Word8+    pokeByteOff p 0 w1+    pokeByteOff p 1 w2+    pokeByteOff p 2 w3++instance Storable Word24 where+  sizeOf _    = 3+  alignment _ = 3+  peek        = readWord24OffPtr+  poke        = writeWord24ToPtr+
+ src/Database/MySQL/Authentication.hs view
@@ -0,0 +1,65 @@+-- | Explicit external authentication settings. PAM replies are cleartext and+-- require TLS or a Unix socket. GSSAPI uses existing system credentials, never+-- the SQL password, and requires an exact expected server principal.+--+-- Pass these options to @connectWithAuthentication@ in+-- "Database.MySQL.Base" or "Database.MySQL.TLS". Defaults never invoke external+-- credential providers. The caller configures server plugins, PAM/realm policy+-- and credentials separately; the driver does not acquire an initial ticket.+-- GSSAPI requires @-threaded@ and macOS GSS.framework or MIT Kerberos on Linux;+-- Windows GSSAPI is unavailable. Use TLS for SQL traffic confidentiality even+-- when the login itself uses mutually authenticated Kerberos.+--+-- Dialogs allow at most 16 exchanges and 65,535-byte prompts/replies, with no+-- NUL in replies. Authentication messages have an additional bounded wire+-- budget. Callback exceptions retain their identity and close the transport.+-- See the <https://github.com/aantich/mariadb-haskell/blob/main/docs/USER_GUIDE.md user guide>.+module Database.MySQL.Authentication+    ( AuthenticationOptions(..)+    , defaultAuthenticationOptions+    , DialogPrompt(..)+    , passwordDialog+    ) where++import Data.ByteString (ByteString)++-- | Authentication-only settings, discarded after establishment. Callbacks run+-- on the connecting thread under the connection deadline; exceptions propagate+-- and close the transport. Do not log their replies or the SQL credentials.+data AuthenticationOptions = AuthenticationOptions+    { authenticationDialog :: Maybe (DialogPrompt -> IO ByteString)+      -- ^ Opt in to PAM/dialog and clear-password replies. No terminal fallback.+    , authenticationGSSAPIPrincipal :: Maybe ByteString+      -- ^ Require GSSAPI with this exact service principal, including realm.+      -- Password-only success and alternate plugin switches are rejected;+      -- no initial password proof is emitted before GSSAPI negotiation.+      -- Native GSSAPI requires an application built with @-threaded@ so a KDC+      -- call cannot prevent the connection deadline or cancellation handler.+    }++-- | Disable external credential use until the application opts in explicitly.+defaultAuthenticationOptions :: AuthenticationOptions+defaultAuthenticationOptions = AuthenticationOptions Nothing Nothing++-- | One MariaDB dialog question. Text is the server's uninterpreted prompt,+-- without its terminal NUL. Round numbers start at zero. A final question still+-- requires the server's OK before authentication succeeds.+data DialogPrompt = DialogPrompt+    { dialogText :: !ByteString+      -- ^ Uninterpreted server bytes; do not render as trusted terminal controls.+    , dialogEcho :: !Bool+      -- ^ True for a visible answer, False for a hidden credential prompt.+    , dialogLast :: !Bool+      -- ^ The server marks its last question; a terminal OK is still required.+    , dialogRound :: !Int+      -- ^ Zero-based exchange number, useful for explicit multi-factor policy.+    } deriving (Eq, Show)++-- | A noninteractive single-password PAM callback. Only the first hidden prompt+-- is accepted; a PIN, username or second factor needs an application callback.+-- Embedded NUL and response-size checks are applied by the authentication loop.+passwordDialog :: ByteString -> DialogPrompt -> IO ByteString+passwordDialog password prompt+    | not (dialogEcho prompt) && dialogRound prompt == 0 = pure password+    | otherwise = ioError (userError "PAM requires an application dialog callback")+
+ src/Database/MySQL/Authentication/GSSAPI.hs view
@@ -0,0 +1,112 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE ForeignFunctionInterface #-}+{-# LANGUAGE ScopedTypeVariables #-}+-- | Internal platform provider. SQL transport and authentication policy remain+-- in Connection; this module owns only native context lifetime and token steps.+module Database.MySQL.Authentication.GSSAPI (GSSAPIError(..), withGSSAPI) where++import Control.Exception+import Data.ByteString (ByteString)++#ifndef mingw32_HOST_OS+import Control.Concurrent+import Control.Monad+import qualified Data.ByteString as B+import Foreign hiding (void)+import Foreign.C.Types++#endif++newtype GSSAPIError = GSSAPIError String deriving Show+instance Exception GSSAPIError++#ifdef mingw32_HOST_OS+-- Windows needs an SSPI provider; do not fail every ordinary-driver build by+-- attempting to link Unix GSSAPI libraries on that platform.+withGSSAPI :: ByteString -> Bool -> ((ByteString -> IO (Bool, ByteString)) -> IO a) -> IO a+withGSSAPI _ _ _ = throwIO (GSSAPIError "native GSSAPI requires a Unix platform provider")+#else+-- A worker is the sole owner of native state. Cancelling the connecting thread+-- enqueues Stop without waiting for a potentially blocking KDC request. The+-- worker finishes that request, releases its context and exits; it has no socket+-- access and cannot continue authentication after the caller's transport closes.+-- This also prevents a finalizer racing gss_init_sec_context in a safe FFI call.+data Request = Step ByteString (MVar (Either SomeException (Bool, ByteString))) | Stop+data NativeContext++foreign import ccall safe "mariadb_gss_open"+    nativeOpen :: Ptr CChar -> CSize -> CInt -> Ptr (Ptr NativeContext)+               -> Ptr Word32 -> Ptr Word32 -> IO CInt+foreign import ccall safe "mariadb_gss_step"+    nativeStep :: Ptr NativeContext -> Ptr CChar -> CSize -> Ptr (Ptr CChar)+               -> Ptr CSize -> Ptr CInt -> Ptr Word32 -> Ptr Word32 -> IO CInt+foreign import ccall safe "mariadb_gss_close"+    nativeClose :: Ptr NativeContext -> IO ()++-- | Internal provider boundary. One serialized step yields completion plus an+-- owned token copy. All output is bounded to 1 MiB. Native work already in+-- progress can outlive cancellation, subject to the OS Kerberos retry policy;+-- cancellation itself never waits for it and never exposes the socket to it.+withGSSAPI :: ByteString -> Bool -> ((ByteString -> IO (Bool, ByteString)) -> IO a) -> IO a+withGSSAPI principal negotiate action = mask $ \restore -> do+    -- A non-threaded RTS cannot run the timeout handler while another Haskell+    -- thread is inside a safe foreign call. Fail before acquiring credentials+    -- rather than silently weakening the connection deadline.+    unless rtsSupportsBoundThreads+        (throwIO (GSSAPIError "GSSAPI authentication requires an application built with -threaded"))+    requests <- newEmptyMVar+    ready <- newEmptyMVar+    _ <- forkIO $ do+        opened <- try (openContext principal negotiate)+        putMVar ready opened+        case opened of+            Left (_ :: SomeException) -> pure ()+            Right context -> worker requests context `finally` nativeClose context+    -- A cancelled caller can leave Step in the single-slot queue. Queue Stop+    -- from a helper rather than dropping it or blocking connection cleanup.+    let stop = void (forkIO (putMVar requests Stop))+        step token = do+            reply <- newEmptyMVar+            putMVar requests (Step token reply)+            takeMVar reply >>= either throwIO pure+    flip finally stop $ do+        restore (takeMVar ready) >>= either throwIO (const (pure ()))+        restore (action step)+  where+    worker requests context = do+        request <- takeMVar requests+        case request of+            Stop -> pure ()+            Step token reply -> do+                result <- try (stepContext context token)+                putMVar reply result+                worker requests context++openContext :: ByteString -> Bool -> IO (Ptr NativeContext)+openContext principal negotiate = B.useAsCStringLen principal $ \(name,size) ->+    alloca $ \result -> alloca $ \major -> alloca $ \minor -> do+        status <- nativeOpen name (fromIntegral size) (if negotiate then 1 else 0) result major minor+        checkNative status major minor+        peek result++stepContext :: Ptr NativeContext -> ByteString -> IO (Bool, ByteString)+stepContext context token = B.useAsCStringLen token $ \(input,size) ->+    alloca $ \output -> alloca $ \lengthPtr -> alloca $ \complete ->+    alloca $ \major -> alloca $ \minor -> do+        status <- nativeStep context input (fromIntegral size) output lengthPtr complete major minor+        checkNative status major minor+        count <- fromIntegral <$> peek lengthPtr+        bytes <- if count == 0 then pure B.empty else peek output >>= \ptr -> B.packCStringLen (ptr,count)+        done <- (/= 0) <$> peek complete+        pure (done,bytes)++checkNative :: CInt -> Ptr Word32 -> Ptr Word32 -> IO ()+checkNative status major minor = unless (status == 0) $ do+    majorCode <- peek major+    minorCode <- peek minor+    -- Numeric provider codes retain diagnostics without logging ticket bodies,+    -- credentials, cache names or server-controlled challenge text.+    throwIO (GSSAPIError ("GSSAPI authentication failed (status " ++ show status +++        ", major " ++ show majorCode ++ ", minor " ++ show minorCode ++ ")"))++#endif
+ src/Database/MySQL/Base.hs view
@@ -0,0 +1,1120 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-|+Module      : Database.MySQL.Base+Description : Public MariaDB connection, query and transaction API+Copyright   : (c) Winterland, 2016+License     : BSD+Maintainer  : Anton Antich <anton.antich@gmail.com>+Stability   : experimental+Portability : macOS and Linux tested++__Alpha software.__ Automated validation is extensive, but this fork has not+yet been validated in real-world applications. Evaluate your own workloads;+APIs and behavior may change. See the linked guide for tested scope and limits.++Start here for MariaDB application code. The historical @MySQL@ module/type+names are retained API identifiers; MySQL servers are not a support target.+See the <https://github.com/aantich/mariadb-haskell/blob/main/docs/USER_GUIDE.md user guide>+and <https://github.com/aantich/mariadb-haskell/tree/main/examples complete examples>.++= Choosing an API++* Use 'connect' for TCP, 'connectUnixSocket' for local sockets, or+  "Database.MySQL.TLS" for verified TLS. Defaults select @utf8mb4@.+* Use 'withPreparedStatement' and 'queryPrepared'/'executePrepared' for checked+  binary bindings. Placeholders bind values, never identifiers or SQL fragments.+* Use 'query_' for literal SQL, 'query' for session-aware text bindings, and+  'queryPreparedMulti' for bounded multi-result procedures/OUT parameters.+* Use 'executeBulk' for native batches, 'executePreparedStream' for one-shot+  large inputs, and 'withCursor' for application-controlled server fetches.+* Use 'resetConnectionWith' for clean external-pool reuse and 'getSessionState'+  for no-SQL observations. Reset invalidates prepared handles and cursors.+* Use "Database.MySQL.BinLog" on a dedicated connection for CDC.++Managed metadata caching/session tracking default on. Compression and progress+negotiation default off; set t'ConnectionOptions' before connecting. Native+capabilities depend on the server's offered flags/version and configuration.++= Ownership and failures++With @OverloadedStrings@, a typical prepared row consumer is:++> withPreparedStatement conn "SELECT name FROM customers WHERE id = ?" $ \statement -> do+>     (_, rows) <- queryPrepared statement [MySQLInt64 customerId]+>     Stream.foldM (\() row -> print row) () rows++Here @conn@ is bracketed by the caller and @Stream@ is a qualified import of+"System.IO.Streams". Never let @rows@ escape the statement bracket.++One connection admits one command or unfinished result stream at a time. Read a+stream on the thread that created it and consume it before issuing another+command. Use separate connections for concurrent work.++A complete t'ERRException' leaves the wire synchronized, but invalidates cached+session facts. Explicitly call 'refreshSessionState' before rendering further+text parameters. Transport failures, malformed replies and cancellation during+I/O retire the connection; inspect 'connectionStatus' and open a new connection.++t'WrongParamsCount' reports placeholder arity errors. t'ExtraResultSets' means a+single-result API drained results that it could not return. 'withTransaction'+documents rollback and uncertain-COMMIT behavior separately.++-}+module Database.MySQL.Base+    ( -- * Connections, lifecycle and session state+      MySQLConn+    , ConnectInfo(..)+    , ConnectionOptions(..)+    , defaultConnectionOptions+    , AuthenticationOptions(..)+    , defaultAuthenticationOptions+    , DialogPrompt(..)+    , passwordDialog+    , connectWithAuthentication+    , connectDetailWithAuthentication+    , connectUnixSocketWithAuthentication+    , connectUnixSocketDetailWithAuthentication+    , connectWithOptions+    , connectDetailWithOptions+    , connectUnixSocketWithOptions+    , connectUnixSocketDetailWithOptions+    , ConnectionStatus(..)+    , ConnectionFailure(..)+    , ConnectionError(..)+    , connectionStatus+    , connectionFailure+    , Progress(..)+    , setProgressCallback+    , resetConnection+    , resetConnectionWith+    , getSessionState+    , SessionState(..)+    , SessionChange(..)+    , refreshSessionState+    , getRenderContext+    , RenderContext(..)+    , defaultRenderContext+    , ParameterError(..)+    , defaultConnectInfo+    , defaultConnectInfoMB4+    , connect+    , connectDetail+    , connectUnixSocket+    , connectUnixSocketDetail+    , close+    , ping+      -- * Text queries and results+    , execute+    , executeMany+    , executeMany_+    , execute_+    , query_+    , queryVector_+    , query+    , queryVector+    , queryMulti_+    , queryMulti+    , StatementResult(..)+      -- * Binary prepared statements, uploads, cursors and bulk+    , prepareStmt+    , prepareStmtDetail+    , executeStmt+    , queryStmt+    , queryStmtVector+    , closeStmt+    , resetStmt+    , PreparedStatement+    , StatementException(..)+    , prepareStatement+    , withPreparedStatement+    , preparedMetadata+    , closePrepared+    , resetPrepared+    , queryPrepared+    , executePrepared+    , PreparedParameter(..)+    , executePreparedStream+    , queryPreparedStream+    , queryPreparedStreamMulti+    , PreparedCursor+    , openCursor+    , openCursorStream+    , fetchCursor+    , closeCursor+    , cursorExhausted+    , withCursor+    , withCursorStream+    , queryStmtMulti+    , queryPreparedMulti+    , PreparedResult(..)+    , executeBulk+    , executeBulkUnitResults+    , BulkUnitResult(..)+      -- * Helpers+    , withTransaction+    , QueryParam(..)+    , Param (..)+    , Query(..)+    , renderParams+    , command+    , Stream.skipToEof+      -- * Exceptions+    , NetworkException(..)+    , AuthException(..)+    , UnconsumedResultSet(..)+    , ERRException(..)+    , UnexpectedPacket(..)+    , DecodePacketException(..)+    , CompressionException(..)+    , WrongParamsCount(..)+    , ExtraResultSets(..)+      -- * MariaDB wire types (historical identifiers retained)+    , module  Database.MySQL.Protocol.Auth+    , module  Database.MySQL.Protocol.Command+    , module  Database.MySQL.Protocol.ColumnDef+    , module  Database.MySQL.Protocol.Packet+    , module  Database.MySQL.Protocol.MySQLValue+    ) where++import           Control.Exception+import           Control.Monad+import           Data.Binary                        (Get)+import Data.Binary.Put (runPut, putWord8)+import Data.Word (Word8, Word16, Word32, Word64)+import qualified Data.ByteString as B+import qualified Data.Text as T+import Control.Concurrent (myThreadId)+import qualified Data.IntMap.Strict as IM+import Data.List (transpose, find)+import System.Timeout (timeout)+import           Data.Bits                          ((.&.))+import           Data.IORef                         (IORef, newIORef, readIORef, writeIORef, atomicModifyIORef')+import           Database.MySQL.Authentication+import           Database.MySQL.Connection+import           Database.MySQL.Protocol.Auth+import           Database.MySQL.Protocol.ColumnDef+import           Database.MySQL.Protocol.Command+import           Database.MySQL.Protocol.MySQLValue+import           Database.MySQL.Protocol.Packet+import           Database.MySQL.Protocol.Progress (Progress(..))+import           Database.MySQL.Protocol.Compression (CompressionException(..))++import           Database.MySQL.Session (SessionState(..), SessionChange(..))+import           Database.MySQL.Query+import           System.IO.Streams                  (InputStream)+import qualified System.IO.Streams                  as Stream+import qualified Data.Vector                        as V++--------------------------------------------------------------------------------+++-- | Render parameters with the connection's current session context and execute+-- one statement. Unexpected row results are drained before t'ExtraResultSets'.+execute :: QueryParam p => MySQLConn -> Query -> [p] -> IO OK+execute conn qry values = withCommand conn $ do+    rendered <- connectedRender conn qry values+    executeCore conn (COM_QUERY (fromQuery rendered))+-- | Execute SQL bytes without parameter rendering. The caller owns SQL quoting.+execute_ :: MySQLConn -> Query -> IO OK+execute_ conn (Query qry) = withCommand conn (executeCore conn (COM_QUERY qry))+-- | Execute raw multi-statement SQL and return its OK replies. Row results are+-- drained before reporting t'ExtraResultSets'; this operation is not atomic.+executeMany_ :: MySQLConn -> Query -> IO [OK]+executeMany_ conn (Query qry) = withCommand conn $ do+    sendCommand conn (COM_QUERY qry)+    readStatementResults conn False >>= onlyOKs+-- | Compile a stable template once and send every parameter set in one command.+-- Ambiguous context-changing templates and invalid values fail before writing.+-- A later server error can leave earlier statements executed; use an explicit+-- transaction when all-or-nothing execution is required. Every server OK reply+-- is returned, including an empty/comment-only result after a final delimiter.+executeMany :: QueryParam p => MySQLConn -> Query -> [[p]] -> IO [OK]+executeMany _ _ [] = pure []+executeMany conn qry values = withCommand conn $ do+    ctx <- getRenderContext conn+    plan <- checked (compileQuery ctx qry)+    unless (planBatchSafe plan) (throwIO AmbiguousParameterizedBatch)+    writeIORef (connectionQueryEffect conn) (Just False)+    rendered <- traverse (checked . renderPlanPut ctx plan) values+    let separator+            | planTerminated plan = putWord8 10+            | otherwise = putWord8 10 >> putWord8 59+        joinBatch []=pure ()+        joinBatch [one]=one+        -- End any trailing line comment before writing the statement separator.+        -- Without the leading newline, '-- ...' or '# ...' can swallow the+        -- separator and all subsequent statements in an otherwise valid batch.+        -- A template may already end with ';', followed by an optional comment.+        -- Preserve that delimiter instead of creating an empty middle statement.+        joinBatch (one:rest)=one >> separator >> joinBatch rest+        bytes=runPut (joinBatch rendered)+    sendCommand conn (COM_QUERY bytes)+    readStatementResults conn False >>= onlyOKs++checked :: Either ParameterError a -> IO a+checked (Left ParameterCountMismatch) = throwIO WrongParamsCount+checked (Left err) = throwIO err+checked (Right value) = pure value++connectedRender :: QueryParam p => MySQLConn -> Query -> [p] -> IO Query+connectedRender conn qry values = do+    ctx <- getRenderContext conn+    plan <- checked (compileQuery ctx qry)+    -- A multi-statement template must have a stable rendering context throughout.+    when (not (planBatchSafe plan) && planMultiple plan) (throwIO AmbiguousParameterizedBatch)+    writeIORef (connectionQueryEffect conn) (Just (planOpaque plan))+    checked (renderPlan ctx plan values)++-- | Stream the first row result using checked text parameters. The stream owns+-- the connection until EOF and must be consumed on the calling thread. Later+-- row results are drained before t'ExtraResultSets'; use 'queryMulti' to keep them.+query :: QueryParam p => MySQLConn -> Query -> [p] -> IO ([ColumnDef], InputStream [MySQLValue])+query conn qry values = withCommand conn $ do+    rendered <- connectedRender conn qry values+    queryCore conn (COM_QUERY (fromQuery rendered)) False+-- | Raw-SQL variant of 'query', with the same stream ownership contract.+query_ :: MySQLConn -> Query -> IO ([ColumnDef],InputStream [MySQLValue])+query_ conn (Query qry) = withCommand conn (queryCore conn (COM_QUERY qry) False)+-- | Vector variant of 'query'; rows share the checked list decoder.+queryVector :: QueryParam p => MySQLConn -> Query -> [p] -> IO (V.Vector ColumnDef,InputStream (V.Vector MySQLValue))+queryVector conn qry values = withCommand conn $ do+    rendered <- connectedRender conn qry values+    queryVectorCore conn (COM_QUERY (fromQuery rendered)) False+-- | Raw-SQL vector stream, with the same ownership contract as 'query'.+queryVector_ :: MySQLConn -> Query -> IO (V.Vector ColumnDef,InputStream (V.Vector MySQLValue))+queryVector_ conn (Query qry) = withCommand conn (queryVectorCore conn (COM_QUERY qry) False)++-- | Results have been drained, but the selected API could not return their shape.+data ExtraResultSets = ExtraResultSets deriving (Show)+instance Exception ExtraResultSets++-- | One fully materialized statement result. Multi-result APIs retain all rows+-- in memory; use the streaming APIs for large individual result sets.+data StatementResult = StatementOK OK | StatementRows [ColumnDef] [[MySQLValue]] deriving (Show,Eq)+-- | Render a stable multi-statement template and collect every result.+queryMulti :: QueryParam p => MySQLConn -> Query -> [p] -> IO [StatementResult]+queryMulti conn qry values = withCommand conn $ do+    rendered <- connectedRender conn qry values+    sendCommand conn (COM_QUERY (fromQuery rendered))+    readStatementResults conn False+-- | Collect every result of raw multi-statement SQL, retaining its rows in memory.+queryMulti_ :: MySQLConn -> Query -> IO [StatementResult]+queryMulti_ conn (Query qry) = withCommand conn $ sendCommand conn (COM_QUERY qry) >> readStatementResults conn False++moreResults :: Word16 -> Bool+moreResults flags = flags .&. 8 /= 0++readStatementResults :: MySQLConn -> Bool -> IO [StatementResult]+readStatementResults conn binary = loop []+  where+    loop earlier = do+        packet <- readConnPacket conn+        if isERR packet then serverError conn packet+        else if isOK packet then do+            ok <- consumeOK conn packet+            let results=StatementOK ok:earlier+            if moreResults (okStatus ok) then loop results else finishResponse conn >> pure (reverse results)+        else do+            columns <- readResultColumns conn packet+            let fields = cachedFields columns+                decode=if binary then decodeBinaryRow (cachedBinaryDecoder columns) else decodeTextRow (prepareTextRow fields)+                rows acc = do+                    next <- readConnPacket conn+                    done <- isResultEnd conn next+                    if done then (,) (reverse acc) <$> consumeEnd conn next+                    else if isERR next then serverError conn next else decode next >>= \row -> rows (row:acc)+            (values,flags) <- rows []+            let results=StatementRows fields values:earlier+            if moreResults flags then loop results else finishResponse conn >> pure (reverse results)++onlyOKs :: [StatementResult] -> IO [OK]+onlyOKs = traverse $ \result -> case result of StatementOK ok -> pure ok; _ -> throwIO ExtraResultSets++executeCore :: MySQLConn -> Command -> IO OK+executeCore conn cmd = do+    sendCommand conn cmd+    packet <- readConnPacket conn+    if isERR packet then serverError conn packet+    else if isOK packet then do+        ok <- consumeOK conn packet+        extra <- if moreResults (okStatus ok) then skipFurtherResults conn (isBinaryCommand cmd) False else pure False+        finishResponse conn+        when extra (throwIO ExtraResultSets)+        pure ok+    else do+        void (readResultColumns conn packet)+        let skip = do+                next <- readConnPacket conn+                done <- isResultEnd conn next+                if done then consumeEnd conn next else if isERR next then serverError conn next else skip+        flags <- skip+        when (moreResults flags) (void (skipFurtherResults conn (isBinaryCommand cmd) True))+        finishResponse conn+        throwIO ExtraResultSets++isBinaryCommand :: Command -> Bool+isBinaryCommand (COM_STMT_EXECUTE _ _ _) = True+isBinaryCommand (COM_STMT_EXECUTE_EXT _ _ _ _) = True+isBinaryCommand (COM_STMT_BULK_EXECUTE _ _ _ _) = True+isBinaryCommand _ = False++data QueryReply = WithoutResultSet | ResultSetColumns CachedColumns++readQueryReply :: MySQLConn -> IO QueryReply+readQueryReply conn = do+    packet <- readConnPacket conn+    if isERR packet then serverError conn packet+    else if isOK packet then do+        ok <- consumeOK conn packet+        if moreResults (okStatus ok) then readQueryReply conn else finishResponse conn >> pure WithoutResultSet+    else do+        ResultSetColumns <$> readResultColumns conn packet++queryCore :: MySQLConn -> Command -> Bool -> IO ([ColumnDef],InputStream [MySQLValue])+queryCore conn cmd binary = do+    sendCommand conn cmd+    reply <- readQueryReply conn+    case reply of+        WithoutResultSet -> (,) [] <$> Stream.nullInput+        ResultSetColumns columns -> do+            let fields = cachedFields columns+                decode=if binary then decodeBinaryRow (cachedBinaryDecoder columns) else decodeTextRow (prepareTextRow fields)+            stream <- resultSetRows conn binary decode+            pure (fields,stream)++queryVectorCore :: MySQLConn -> Command -> Bool -> IO (V.Vector ColumnDef,InputStream (V.Vector MySQLValue))+queryVectorCore conn cmd binary = do+    sendCommand conn cmd+    reply <- readQueryReply conn+    case reply of+        WithoutResultSet -> (,) V.empty <$> Stream.nullInput+        ResultSetColumns columns -> do+            let fieldList = cachedFields columns+                fields=V.fromList fieldList+                decodeList=if binary then decodeBinaryRow (cachedBinaryDecoder columns) else decodeTextRow (prepareTextRow fieldList)+                decode packet = V.fromListN (cachedColumnCount columns) <$> decodeList packet+            stream <- resultSetRows conn binary decode+            pure (fields,stream)++-- The generation and owner protect both ordinary reads and rollback draining.+-- Draining validates framing/terminators but deliberately avoids decoding rows+-- that the application will never inspect. No row buffers are retained here.+resultSetRows :: MySQLConn -> Bool -> (Packet -> IO row) -> IO (InputStream row)+resultSetRows conn binary decode = do+    modern <- usesOKTerminator conn+    generation <- markStreaming conn+    finished <- newIORef False+    stream <- Stream.makeInputStream $ do+        done <- readIORef finished+        if done then pure Nothing else streamGuard conn generation $ do+            packet <- readConnPacket conn+            let ending = isResultEndFor modern packet+            if ending then do+                writeIORef finished True+                flags <- consumeEnd conn packet+                extra <- if moreResults flags then skipFurtherResults conn binary False else pure False+                finishResponse conn+                when extra (throwIO ExtraResultSets)+                pure Nothing+            else if isERR packet then writeIORef finished True >> serverError conn packet+            else Just <$> decode packet+    let drain = do+            done <- readIORef finished+            unless done $ streamGuard conn generation $ do+                packet <- readConnPacket conn+                let ending = isResultEndFor modern packet+                if ending then do+                    flags <- consumeEnd conn packet+                    when (moreResults flags) (void (skipFurtherResults conn binary False))+                    writeIORef finished True+                    finishResponse conn+                else if isERR packet then writeIORef finished True >> serverError conn packet else drain+    writeIORef (connectionDrain conn) (Just drain)+    pure stream++skipFurtherResults :: MySQLConn -> Bool -> Bool -> IO Bool+skipFurtherResults conn binary seen = do+    packet <- readConnPacket conn+    if isERR packet then serverError conn packet+    else if isOK packet then do+        ok <- consumeOK conn packet+        if moreResults (okStatus ok) then skipFurtherResults conn binary seen else pure seen+    else do+        void (readResultColumns conn packet)+        let skip = do+                row <- readConnPacket conn+                end <- isResultEnd conn row+                if end then consumeEnd conn row else if isERR row then serverError conn row else skip+        flags <- skip+        if moreResults flags then skipFurtherResults conn binary True else pure True++decodeBinaryRow :: Get [MySQLValue] -> Packet -> IO [MySQLValue]+decodeBinaryRow decoder packet = if isOK packet then decodeTextRow decoder packet else throwIO (UnexpectedPacket packet)++decodeTextRow :: Get [MySQLValue] -> Packet -> IO [MySQLValue]+decodeTextRow parser packet = do+    values <- getFromPacketExact parser packet+    -- Each constructor has strict payloads. Force the whole spine and every+    -- constructor here so invalid UTF-8/numbers fail while streamGuard still+    -- owns the response, rather than as a delayed exception in application code.+    _ <- evaluate (forceValues values)+    pure values+  where+    forceValues []=()+    forceValues (value:rest)=value `seq` forceValues rest+{-# INLINE decodeTextRow #-}++-- | Prepare server-side SQL. Statement IDs belong to this connection; close+-- them explicitly with 'closeStmt' after consuming their results.+prepareStmt :: MySQLConn -> Query -> IO StmtID+prepareStmt conn qry = do+    (StmtPrepareOK ident _ _ _,_,_) <- prepareStmtDetail conn qry+    pure ident+-- | Prepare SQL and return parameter metadata followed by result metadata.+prepareStmtDetail :: MySQLConn -> Query -> IO (StmtPrepareOK,[ColumnDef],[ColumnDef])+prepareStmtDetail conn (Query stmt) = withCommand conn $ do+    states <- statementMap conn+    when (IM.size states >= maximumPreparedStatements (connectionOptions conn))+        (throwIO (ProtocolError "prepared statement registry exceeds client limit"))+    sendCommand conn (COM_STMT_PREPARE stmt)+    packet <- readConnPacket conn+    when (isERR packet) (serverError conn packet)+    info@(StmtPrepareOK _ columns params _) <- getFromPacketExact getStmtPrepareOK packet+    let StmtPrepareOK ident _ _ _=info+    effect <- readIORef (connectionQueryEffect conn)+    prior <- readIORef (connectionOpaqueStatements conn)+    let rest=filter (/=ident) prior+    when (effect==Just True && length rest>=maximumColumns (connectionOptions conn))+        (throwIO (ProtocolError "opaque prepared statement registry exceeds bound"))+    writeIORef (connectionOpaqueStatements conn) (if effect==Just True then ident:rest else rest)+    -- Preparing alone has not executed the statement's session effects.+    writeIORef (connectionQueryEffect conn) (Just False)+    pdefs <- readColumns conn params+    metadata <- readColumnBlock conn columns+    next <- atomicModifyIORef' (connectionStatementSerial conn) $ \n ->+        if n == maxBound then (n, Nothing) else (n + 1, Just (n + 1))+    serial <- maybe (throwIO (ProtocolError "statement registration generation exhausted")) pure next+    registerStatement conn ident serial params+    retained <- storeStatementColumns conn ident 0 metadata+    pure (info,pdefs,cachedFields retained)+-- | Release a statement. This command has no server reply and cannot run while+-- a result is pending. Raw IDs cannot detect reuse by a later server statement.+closeStmt :: MySQLConn -> StmtID -> IO ()+closeStmt conn ident = withCommand conn $ do+    sendCommand conn (COM_STMT_CLOSE ident)+    forgetStatement conn ident+    atomicModifyIORef' (connectionOpaqueStatements conn) (\ids -> (filter (/=ident) ids,()))+-- | Reset a statement and consume its own reply; pending rows must be read first.+resetStmt :: MySQLConn -> StmtID -> IO ()+resetStmt conn ident = void (command conn (COM_STMT_RESET ident))+-- | Execute with binary parameters. Text values require a known client charset;+-- numeric/binary values do not require a rendering snapshot.+executeStmt :: MySQLConn -> StmtID -> [MySQLValue] -> IO OK+executeStmt conn ident values = withCommand conn $ do+    validateStmtArity conn ident values+    executeCore conn (COM_STMT_EXECUTE ident values (makeNullMap values))+-- | Stream prepared binary rows under the ownership contract of 'query'.+queryStmt :: MySQLConn -> StmtID -> [MySQLValue] -> IO ([ColumnDef],InputStream [MySQLValue])+queryStmt conn ident values = withCommand conn $ do+    validateStmtArity conn ident values+    queryCore conn (COM_STMT_EXECUTE ident values (makeNullMap values)) True+-- | Vector variant of 'queryStmt'.+queryStmtVector :: MySQLConn -> StmtID -> [MySQLValue] -> IO (V.Vector ColumnDef,InputStream (V.Vector MySQLValue))+queryStmtVector conn ident values = withCommand conn $ do+    validateStmtArity conn ident values+    queryVectorCore conn (COM_STMT_EXECUTE ident values (makeNullMap values)) True++-- | A statement bound to its creating connection and registration generation.+-- The constructor is private: a handle cannot accidentally be executed on a+-- different session, or close a new statement that reused an old numeric ID.+-- Consume streams before close/reset; terminal connection failure invalidates+-- every handle. Raw-ID functions remain available for compatibility.+data PreparedStatement = PreparedStatement !MySQLConn !StmtID !Word64++-- | Statement/parameter misuse. Closed handles, invalid fetch sizes and pending+-- batches fail before writing. 'InvalidCursorUse' can also follow a synchronized+-- non-row reply. A streamed-parameter error arising after upload begins retires+-- its incomplete connection before throwing.+data StatementException = StatementClosed | InvalidBulkBatch String+                        | InvalidPreparedParameter String | InvalidCursorUse String+    deriving (Show)+instance Exception StatementException++-- | Prepare a connection-owned handle. Its initial metadata is available through+-- 'preparedMetadata'; MariaDB may replace that metadata during later execution.+prepareStatement :: MySQLConn -> Query -> IO PreparedStatement+prepareStatement conn queryText = mask_ $ do+    (info, _, _) <- prepareStmtDetail conn queryText+    states <- statementMap conn+    case IM.lookup (fromIntegral (stmtId info)) states of+        Nothing -> throwIO StatementClosed+        Just state -> pure (PreparedStatement conn (stmtId info) (statementSerial state))++-- | Bracket a server statement. Closing a retired connection's handle is a+-- local no-op, so cleanup does not replace the query's transport exception.+-- Abandoned rows and close share one cleanup deadline. Timed I/O restores the+-- caller's masking state; lifetime transitions and failure handling stay masked.+withPreparedStatement :: MySQLConn -> Query -> (PreparedStatement -> IO a) -> IO a+withPreparedStatement conn queryText action = mask $ \restore -> do+    statement <- prepareStatement conn queryText+    outcome <- try (restore (action statement))+    case outcome of+        Right value -> cleanupPrepared restore statement >> pure value+        Left (primary :: SomeException) -> do+            cleanupPrepared restore statement `catch` \(secondary :: SomeException) -> do+                when (isCancellation secondary) (throwIO secondary)+                atomicModifyIORef' (connectionFailureRef conn)+                    (\failure -> (fmap (\f -> f {secondaryCleanupFailure=True}) failure, ()))+            throwIO primary++-- One deadline covers abandoned rows and close. A stalled or malformed drain+-- retires the transport; a new asynchronous cancellation keeps its identity.+-- Restore the caller's masking state only inside the protected timed action:+-- otherwise a continuously buffered result can run past timeout without ever+-- reaching an interruptible wait. Retirement and exception selection stay masked.+cleanupPrepared :: (IO () -> IO ()) -> PreparedStatement -> IO ()+cleanupPrepared restore statement@(PreparedStatement conn _ _) = do+    result <- try $ timeout (cleanupTimeoutMicros (connectionOptions conn)) $ restore $ do+        pending <- readIORef (connectionDrain conn)+        maybe (pure ()) id pending+        closePrepared statement+    case result of+        Right (Just ()) -> pure ()+        Right Nothing -> abortConnection conn False >> throwIO CleanupDeadlineExceeded+        Left (err :: SomeException) -> abortConnection conn False >> throwIO err++checkedStatement :: PreparedStatement -> IO StatementState+checkedStatement (PreparedStatement conn ident serial) = do+    states <- statementMap conn+    case IM.lookup (fromIntegral ident) states of+        Just state | statementSerial state == serial -> pure state+        _ -> throwIO StatementClosed++validateStmtArity :: MySQLConn -> StmtID -> [MySQLValue] -> IO ()+validateStmtArity conn ident values = do+    states <- statementMap conn+    forM_ (IM.lookup (fromIntegral ident) states) $ \state -> validateStatementValues state values++validateStatementValues :: StatementState -> [MySQLValue] -> IO ()+validateStatementValues state values =+    unless (lengthWithin (statementArity state) values == Just (statementArity state))+        (throwIO WrongParamsCount)++-- | Latest first-result metadata, including unknown MariaDB extension tags.+-- Reading metadata performs no network operations. A subsequent execution or reprepare can replace+-- it; the returned immutable snapshot remains valid for the caller.+preparedMetadata :: PreparedStatement -> IO [ColumnMetadata]+preparedMetadata statement = do+    state <- checkedStatement statement+    pure (maybe [] cachedDetails (IM.lookup 0 (statementResults state)))++-- | Idempotently release this handle only. It never closes a later registration+-- that happens to reuse the same raw statement ID.+closePrepared :: PreparedStatement -> IO ()+closePrepared statement@(PreparedStatement conn ident _) = do+    state <- try (checkedStatement statement)+    case state of+        Left StatementClosed -> pure ()+        Left err -> throwIO err+        Right _ -> withCommand conn $ do+            void (checkedStatement statement)+            sendCommand conn (COM_STMT_CLOSE ident)+            forgetStatement conn ident+            atomicModifyIORef' (connectionOpaqueStatements conn) (\ids -> (filter (/= ident) ids, ()))++-- | Clear parameter/cursor state after consuming pending rows. MariaDB retains+-- the statement's metadata-omission state on COM_STMT_RESET, so its matching+-- decoder must also remain cached. Connection reset is a different operation.+resetPrepared :: PreparedStatement -> IO ()+resetPrepared statement@(PreparedStatement conn ident _) = withCommand conn $ do+    void (checkedStatement statement)+    sendCommand conn (COM_STMT_RESET ident)+    readConnPacket conn >>= consumeCommandOK conn >> pure ()++-- | Stream the first binary row set; later row sets are drained and reported as+-- t'ExtraResultSets'. Use 'queryPreparedMulti' to retain procedure OUT/results.+queryPrepared :: PreparedStatement -> [MySQLValue] -> IO ([ColumnDef], InputStream [MySQLValue])+queryPrepared statement@(PreparedStatement conn ident _) values = withCommand conn $ do+    state <- checkedStatement statement+    validateStatementValues state values+    queryCore conn (COM_STMT_EXECUTE ident values (makeNullMap values)) True++-- | Execute a non-row statement. Unexpected row results are completely drained+-- before t'ExtraResultSets', preserving a synchronized connection.+executePrepared :: PreparedStatement -> [MySQLValue] -> IO OK+executePrepared statement@(PreparedStatement conn ident _) values = withCommand conn $ do+    state <- checkedStatement statement+    validateStatementValues state values+    executeCore conn (COM_STMT_EXECUTE ident values (makeNullMap values))++-- | A scalar binding or a one-shot source for a large binary/text parameter.+-- Text chunks use the current client charset; chunk boundaries need not match+-- wire boundaries. Sources remain owned by the caller and are never replayed+-- or closed by the driver. Empty sources bind an empty, non-NULL value with+-- the corresponding binary or text type/collation.+data PreparedParameter+    = PreparedValue !MySQLValue+      -- ^ An inline scalar. Use 'MySQLNull' for SQL NULL.+    | PreparedBytes !(InputStream B.ByteString)+      -- ^ A one-shot binary source, including an empty non-NULL source.+    | PreparedText !(InputStream T.Text)+      -- ^ A one-shot text source encoded in the current client charset.++-- Upload and EXECUTE hold the same command lease. A no-reply upload command+-- cannot acknowledge server acceptance: MariaDB can defer its error to EXECUTE.+-- Any source/encoding/cancellation failure after upload starts retires the+-- connection, preventing partially accumulated parameters from leaking into a+-- later execution. Limit arithmetic uses subtraction before adding each chunk.+uploadParameters :: PreparedStatement -> [PreparedParameter] -> IO ([ExecuteParameter], BitMap)+uploadParameters statement@(PreparedStatement conn ident _) parameters = do+    state <- checkedStatement statement+    unless (lengthWithin (statementArity state) parameters == Just (statementArity state))+        (throwIO WrongParamsCount)+    let values = map scalar parameters+        scalar (PreparedValue value) = ExecuteValue value+        scalar (PreparedBytes _) = UploadedBytes+        scalar (PreparedText _) = UploadedText+        bitmap = makeNullMap (map executeParameterValue values)+    forM_ [value | ExecuteValue value <- values] $+        either (throwIO . UnencodableParameter) pure . validateValue+    charset <- if any needsCharset parameters then renderClientCharset <$> getRenderContext conn else pure "utf8mb4"+    -- Validate ordinary text too, before consuming any one-shot input.+    forM_ values $ \value -> case value of+        ExecuteValue (MySQLText text) -> void (either (throwIO . UnencodableParameter) pure (encodeCharsetText charset text))+        _ -> pure ()+    serverLimit <- readIORef (connectionServerPacketLimit conn)+    let messageLimit = min serverLimit (maximumMessageBytes (connectionOptions conn))+        totalLimit = min serverLimit (maximumLongDataBytes (connectionOptions conn))+    when (messageLimit <= 7) (throwIO (InvalidPreparedParameter "message limit cannot hold a long-data chunk"))+    remaining <- newIORef totalLimit+    let chunkSize = fromIntegral (min 65536 (messageLimit - 7))+        send index bytes = do+            left <- readIORef remaining+            when (fromIntegral (B.length bytes) > left)+                (throwIO (InvalidPreparedParameter "streamed parameters exceed byte limit"))+            writeIORef remaining (left - fromIntegral (B.length bytes))+            sendCommand conn (COM_STMT_SEND_LONG_DATA ident index bytes)+        upload index source = do+            -- Even a source with no chunks must set MariaDB's long-data state.+            send index B.empty+            -- A source's chunk size is an application detail. Coalesce small+            -- chunks to avoid one command/builder allocation per tiny read,+            -- while bounding retained payload to 64 KiB and segment metadata+            -- to 1024 entries. Large input chunks are sliced without copying.+            let flush [one] = send index one+                flush chunks = send index (B.concat (reverse chunks))+                fill used segments chunks = do+                    next <- Stream.read source+                    case next of+                        Nothing -> unless (used == 0) (flush chunks)+                        Just bytes -> push used segments chunks bytes+                push used segments chunks bytes+                    | B.null bytes = fill used segments chunks+                    | B.length bytes >= chunkSize-used = do+                        let (part,rest) = B.splitAt (chunkSize-used) bytes+                        flush (part:chunks)+                        if B.null rest then fill 0 0 [] else push 0 0 [] rest+                    | segments >= (1023 :: Int) = flush (bytes:chunks) >> fill 0 0 []+                    | otherwise = fill (used+B.length bytes) (segments+1) (bytes:chunks)+            fill 0 0 []+    forM_ (zip [0..] parameters) $ \(index,parameter) -> case parameter of+        PreparedValue _ -> pure ()+        PreparedBytes source -> upload index source+        PreparedText source -> do+            pending <- newIORef T.empty+            let encodePart text = do+                    let (part,rest) = T.splitAt 8192 text+                    writeIORef pending rest+                    Just <$> either (throwIO . UnencodableParameter) pure (encodeCharsetText charset part)+                next = do+                    rest <- readIORef pending+                    if T.null rest then Stream.read source >>= maybe (pure Nothing) encodePart+                        else encodePart rest+            encoded <- Stream.makeInputStream next+            upload index encoded+    pure (values,bitmap)+  where+    needsCharset (PreparedText _) = True+    needsCharset (PreparedValue (MySQLText _)) = True+    needsCharset _ = False++uploadCommand :: PreparedStatement -> Word8 -> [PreparedParameter] -> IO Command+uploadCommand statement@(PreparedStatement _ ident _) flags parameters = do+    (values,bitmap) <- uploadParameters statement parameters+    pure (COM_STMT_EXECUTE_EXT ident flags values bitmap)++-- | Upload bounded one-shot parameters and execute a non-row statement.+-- Server packet/parameter limits still apply; chunking reduces client buffering,+-- not MariaDB's accumulated-value limits. Use a transaction for atomic writes.+executePreparedStream :: PreparedStatement -> [PreparedParameter] -> IO OK+executePreparedStream statement@(PreparedStatement conn _ _) parameters = withCommand conn $ do+    cmd <- uploadCommand statement 0 parameters+    executeCore conn cmd++-- | Stream the first binary row result with uploaded parameters. The returned+-- result has the same owner, lifetime and extra-result rules as 'queryPrepared'.+queryPreparedStream :: PreparedStatement -> [PreparedParameter] -> IO ([ColumnDef], InputStream [MySQLValue])+queryPreparedStream statement@(PreparedStatement conn _ _) parameters = withCommand conn $ do+    cmd <- uploadCommand statement 0 parameters+    queryCore conn cmd True++-- | Upload parameters and collect complete prepared results, including OUT+-- parameters. This materializes result rows, as 'queryPreparedMulti' does.+queryPreparedStreamMulti :: PreparedStatement -> [PreparedParameter] -> IO [PreparedResult]+queryPreparedStreamMulti statement@(PreparedStatement conn _ _) parameters = withCommand conn $ do+    cmd <- uploadCommand statement 0 parameters+    sendCommand conn cmd+    readPreparedResults conn++data CursorPhase = CursorOpen | CursorFetching | CursorFinished deriving Eq++-- | An exclusive, read-only server cursor. Its creating thread owns the+-- connection until the cursor is exhausted or closed. Each fetch returns an+-- incremental stream; finish that batch before requesting another. MariaDB may+-- materialize the result on the server. A cursor is not a snapshot guarantee.+data PreparedCursor = PreparedCursor+    { cursorStatement :: !PreparedStatement+    , cursorGeneration :: !Word64+    , cursorColumns :: !CachedColumns+    , cursorPhase :: !(IORef CursorPhase)+    , cursorBatchDrain :: !(IORef (Maybe (IO ())))+    }++-- | Open a read-only cursor on a prepared SELECT. The metadata cache is shared+-- with ordinary executions. Other SQL commands, including reset/close of the+-- statement, are rejected until exhaustion or 'closeCursor'. The cursor flag+-- does not prove that arbitrary SQL is read-only: use this API with SELECTs;+-- MariaDB can execute other SQL before reporting that no cursor was opened.+openCursor :: PreparedStatement -> [MySQLValue] -> IO ([ColumnDef], PreparedCursor)+openCursor statement@(PreparedStatement conn _ _) values = withCommand conn (openCursorCore statement values)++openCursorCore :: PreparedStatement -> [MySQLValue] -> IO ([ColumnDef], PreparedCursor)+openCursorCore statement@(PreparedStatement _ ident _) values = do+    state <- checkedStatement statement+    validateStatementValues state values+    openCursorCommand statement (COM_STMT_EXECUTE_EXT ident 1 (map ExecuteValue values) (makeNullMap values))++-- | Open a read-only cursor with streamed binary/text parameters. Upload and+-- cursor execution share one lease, with the same one-shot and byte-limit+-- contracts as 'queryPreparedStream'. Consume or close the cursor before reuse.+openCursorStream :: PreparedStatement -> [PreparedParameter] -> IO ([ColumnDef], PreparedCursor)+openCursorStream statement@(PreparedStatement conn _ _) parameters = withCommand conn (openCursorStreamCore statement parameters)++openCursorStreamCore :: PreparedStatement -> [PreparedParameter] -> IO ([ColumnDef], PreparedCursor)+openCursorStreamCore statement parameters = do+    cmd <- uploadCommand statement 1 parameters+    openCursorCommand statement cmd++openCursorCommand :: PreparedStatement -> Command -> IO ([ColumnDef], PreparedCursor)+openCursorCommand statement@(PreparedStatement conn _ _) cmd = do+    sendCommand conn cmd+    packet <- readConnPacket conn+    when (isERR packet) (serverError conn packet)+    when (isOK packet) $ do+        ok <- consumeOK conn packet+        when (moreResults (okStatus ok)) (void (skipFurtherResults conn True False))+        finishResponse conn+        throwIO (InvalidCursorUse "statement did not return a cursor result")+    columns <- readResultColumns conn packet+    modern <- usesOKTerminator conn+    flags <- if modern then readConnPacket conn >>= consumeEnd conn+        else maybe (throwIO (ProtocolError "cursor metadata has no status")) pure =<< readIORef (connectionServerStatus conn)+    unless (flags .&. 64 /= 0 && flags .&. 8 == 0)+        (throwIO (ProtocolError "server did not open a read-only cursor"))+    generation <- markStreaming conn+    phase <- newIORef CursorOpen+    drain <- newIORef Nothing+    let cursor = PreparedCursor statement generation columns phase drain+    writeIORef (connectionDrain conn) (Just (closeCursor cursor))+    pure (cachedFields columns,cursor)++-- | Whether the server has exhausted the cursor, or the caller has closed it.+-- This observation sends no command and does not claim connection ownership.+cursorExhausted :: PreparedCursor -> IO Bool+cursorExhausted cursor = (== CursorFinished) <$> readIORef (cursorPhase cursor)++checkCursorOwner :: PreparedCursor -> IO ()+checkCursorOwner cursor = do+    let PreparedStatement conn _ _ = cursorStatement cursor+    callbackActive <- readIORef (connectionCallbackActive conn)+    when callbackActive (throwIO ConcurrentConnectionUse)+    runtime <- readIORef (connectionRuntime conn)+    tid <- myThreadId+    unless (runtimeGeneration runtime == cursorGeneration cursor) (throwIO StaleResultStream)+    unless (runtimeStatus runtime == Streaming) (throwIO (ConnectionUnavailable (runtimeStatus runtime)))+    unless (runtimeOwner runtime == Just tid) (throwIO ConcurrentConnectionUse)++-- | Request at most this many rows, bounded by 'maximumCursorFetchRows'. Rows+-- are decoded one at a time. An exact-sized final batch may need one final+-- empty fetch before MariaDB reports exhaustion. Zero/excessive counts and an+-- unfinished preceding batch fail locally without sending bytes.+fetchCursor :: PreparedCursor -> Word32 -> IO (InputStream [MySQLValue])+fetchCursor cursor count = do+    let PreparedStatement conn ident _ = cursorStatement cursor+    when (count == 0 || count > maximumCursorFetchRows (connectionOptions conn))+        (throwIO (InvalidCursorUse "invalid cursor fetch size"))+    checkCursorOwner cursor+    phase <- readIORef (cursorPhase cursor)+    unless (phase == CursorOpen) (throwIO (InvalidCursorUse "cursor batch is pending or cursor is closed"))+    streamGuard conn (cursorGeneration cursor) $ do+        sendCommand conn (COM_STMT_FETCH ident count)+        writeIORef (cursorPhase cursor) CursorFetching+        finished <- newIORef False+        received <- newIORef (0 :: Word32)+        let next decode = do+                done <- readIORef finished+                if done then pure Nothing else streamGuard conn (cursorGeneration cursor) $ do+                    packet <- readConnPacket conn+                    ending <- isResultEnd conn packet+                    if ending then do+                        flags <- consumeEnd conn packet+                        unless (flags .&. 8 == 0 && flags .&. 192 `elem` [64,128])+                            (throwIO (ProtocolError "invalid cursor batch status"))+                        writeIORef finished True+                        writeIORef (cursorBatchDrain cursor) Nothing+                        let exhausted = flags .&. 128 /= 0+                        writeIORef (cursorPhase cursor) (if exhausted then CursorFinished else CursorOpen)+                        when exhausted (finishResponse conn)+                        pure Nothing+                    else if isERR packet then do+                        writeIORef (cursorPhase cursor) CursorFinished+                        serverError conn packet+                    else do+                        n <- readIORef received+                        when (n >= count) (throwIO (ProtocolError "cursor sent more rows than requested"))+                        writeIORef received (n+1)+                        Just <$> decode packet+            drain = next (const (pure ())) >>= maybe (pure ()) (const drain)+        writeIORef (cursorBatchDrain cursor) (Just drain)+        Stream.makeInputStream (next (decodeBinaryRow (cachedBinaryDecoder (cursorColumns cursor))))++-- | Drain only an outstanding batch, then reset the statement to release the+-- remaining server cursor without fetching it. One cleanup deadline bounds+-- draining/reset. Repeated close is a local no-op; interruption retires the+-- connection. Metadata remains cached, matching MariaDB's statement reset.+closeCursor :: PreparedCursor -> IO ()+closeCursor cursor = mask $ \restore -> cleanupCursor restore cursor++-- The bracket must enter this helper masked, just as explicit close does.+-- Restoring around the helper call would let a pending second cancellation+-- escape before its failure handler can retire the unfinished connection.+-- Restore only the timed I/O, retaining the caller's original masking state.+cleanupCursor :: (IO () -> IO ()) -> PreparedCursor -> IO ()+cleanupCursor restore cursor = do+    let PreparedStatement conn ident _ = cursorStatement cursor+    status <- connectionStatus conn+    when (status `elem` [Broken,Closed]) $ do+        writeIORef (cursorPhase cursor) CursorFinished+        writeIORef (cursorBatchDrain cursor) Nothing+    done <- cursorExhausted cursor+    unless done $ do+        checkCursorOwner cursor+        result <- try $ timeout (cleanupTimeoutMicros (connectionOptions conn)) $ restore $ streamGuard conn (cursorGeneration cursor) $ do+            pending <- readIORef (cursorBatchDrain cursor)+            maybe (pure ()) id pending+            exhausted <- cursorExhausted cursor+            unless exhausted $ do+                sendCommand conn (COM_STMT_RESET ident)+                ok <- readConnPacket conn >>= consumeCommandOK conn+                when (moreResults (okStatus ok)) (throwIO (ProtocolError "cursor reset returned extra results"))+                writeIORef (cursorPhase cursor) CursorFinished+                finishResponse conn+        case result of+            Right (Just ()) -> pure ()+            Right Nothing -> abortConnection conn False >> throwIO CleanupDeadlineExceeded+            Left (err :: SomeException) -> abortConnection conn False >> throwIO err++-- | Bracket an exclusive cursor, releasing unfinished batches/server state on+-- return or an application exception. A primary exception keeps its identity;+-- a new asynchronous cancellation during cleanup still propagates.+withCursor :: PreparedStatement -> [MySQLValue] -> (PreparedCursor -> IO a) -> IO a+withCursor statement@(PreparedStatement conn _ _) values = withOpenedCursor conn (openCursorCore statement values)++-- | Bracket 'openCursorStream', releasing unfinished batches and the server+-- cursor on return or exception. Caller-owned parameter sources are not closed.+withCursorStream :: PreparedStatement -> [PreparedParameter] -> (PreparedCursor -> IO a) -> IO a+withCursorStream statement@(PreparedStatement conn _ _) parameters = withOpenedCursor conn (openCursorStreamCore statement parameters)++withOpenedCursor :: MySQLConn -> IO ([ColumnDef], PreparedCursor) -> (PreparedCursor -> IO a) -> IO a+withOpenedCursor conn open action = mask $ \restore -> do+    -- Restore acquisition only inside withCommand's failure handler. A fully+    -- buffered one-shot source must remain cancellable, while the return to+    -- bracket bookkeeping stays masked so no cursor can escape its finalizer.+    (_,cursor) <- withCommand conn (restore open)+    result <- try (restore (action cursor))+    case result of+        Right value -> cleanupCursor restore cursor >> pure value+        Left (err :: SomeException) -> do+            cleanupCursor restore cursor `catch` \(secondary :: SomeException) -> when (isCancellation secondary) (throwIO secondary)+            throwIO err++-- | Complete prepared results in wire order. OUT parameters are identified by+-- SERVER_PS_OUT_PARAMS on their terminator, not guessed from column names.+-- These APIs materialize rows; use 'queryPrepared' for large individual streams.+data PreparedResult+    = PreparedOK !OK+    | PreparedRows ![ColumnMetadata] ![[MySQLValue]]+    | PreparedOutParameters ![ColumnMetadata] ![MySQLValue]+    deriving (Show, Eq)++-- | Raw-ID variant of 'queryPreparedMulti'. Unknown/closed IDs are diagnosed by+-- MariaDB, preserving the legacy raw API's server-error contract.+queryStmtMulti :: MySQLConn -> StmtID -> [MySQLValue] -> IO [PreparedResult]+queryStmtMulti conn ident values = withCommand conn $ do+    validateStmtArity conn ident values+    sendCommand conn (COM_STMT_EXECUTE ident values (makeNullMap values))+    readPreparedResults conn++-- | Collect every prepared result, including OUT parameters and trailing OK.+-- All rows are buffered; use 'queryPrepared' for an unbounded single row result.+queryPreparedMulti :: PreparedStatement -> [MySQLValue] -> IO [PreparedResult]+queryPreparedMulti statement@(PreparedStatement conn ident _) values = withCommand conn $ do+    state <- checkedStatement statement+    validateStatementValues state values+    sendCommand conn (COM_STMT_EXECUTE ident values (makeNullMap values))+    readPreparedResults conn++readPreparedResults :: MySQLConn -> IO [PreparedResult]+readPreparedResults conn = loop []+  where+    loop earlier = do+        packet <- readConnPacket conn+        if isERR packet then serverError conn packet+        else if isOK packet then do+            ok <- consumeOK conn packet+            next (okStatus ok) (PreparedOK ok : earlier)+        else do+            columns <- readResultColumns conn packet+            let rows acc = do+                    row <- readConnPacket conn+                    ending <- isResultEnd conn row+                    if ending then (,) (reverse acc) <$> consumeEnd conn row+                    else if isERR row then serverError conn row+                    else decodeBinaryRow (cachedBinaryDecoder columns) row >>= \value -> rows (value : acc)+            (values, flags) <- rows []+            result <- if flags .&. 0x1000 == 0+                then pure (PreparedRows (cachedDetails columns) values)+                else case values of+                    [out] -> pure (PreparedOutParameters (cachedDetails columns) out)+                    _ -> throwIO (ProtocolError "prepared OUT result must contain exactly one row")+            next flags (result : earlier)+    next flags results+        | moreResults flags = loop results+        | otherwise = finishResponse conn >> pure (reverse results)++-- | Per-operation bulk feedback returned by MariaDB, in server order. Aggregate+-- execution instead returns one OK with its total count and first generated ID.+data BulkUnitResult = BulkUnitResult+    { bulkInsertId :: !Word64+    , bulkAffectedRows :: !Word64+    } deriving (Show, Eq)++-- One type per column is a bulk wire invariant. Reject mixed signedness/types+-- before sending; do not silently split into independently committed batches.+bulkCommand :: PreparedStatement -> Word16 -> [[BulkParameter]] -> IO Command+bulkCommand statement@(PreparedStatement conn ident _) flags rows = do+    state <- checkedStatement statement+    let width = statementArity state+        invalid = InvalidBulkBatch "empty/zero-parameter batch or row limit exceeded"+    count <- maybe (throwIO invalid) pure+        (lengthWithin (maximumBulkRows (connectionOptions conn)) rows)+    unless (width > 0 && count > 0) (throwIO invalid)+    ensureBulkMinimumFits conn width count+    forM_ rows $ \row -> unless (lengthWithin width row == Just width)+        (throwIO WrongParamsCount)+    let exemplar column = case find present column of+            Just (BulkValue value) -> value+            _ -> MySQLNull+        present (BulkValue MySQLNull) = False+        present (BulkValue _) = True+        present _ = False+        types = map exemplar (transpose rows)+    pure (COM_STMT_BULK_EXECUTE ident flags types rows)++-- | Execute exactly one bounded native MariaDB binary bulk command. NULL,+-- DEFAULT and IGNORE use native parameter indicators. Errors are not replayed+-- and can follow partial server work: use an explicit transaction for atomicity.+-- Empty batches, mixed column wire types and oversized messages fail locally.+executeBulk :: PreparedStatement -> [[BulkParameter]] -> IO OK+executeBulk statement@(PreparedStatement conn _ _) rows = withCommand conn $ do+    cmd <- bulkCommand statement 128 rows+    executeCore conn cmd++-- | Request MariaDB's negotiated per-operation generated IDs/counts. This is a+-- distinct command mode, not fabricated feedback from an aggregate OK.+executeBulkUnitResults :: PreparedStatement -> [[BulkParameter]] -> IO [BulkUnitResult]+executeBulkUnitResults statement@(PreparedStatement conn _ _) rows = withCommand conn $ do+    cmd <- bulkCommand statement 192 rows+    sendCommand conn cmd+    results <- readPreparedResults conn+    case [values | PreparedRows _ values <- results] of+        [values] | all ordinary results -> mapM unit values+        _ -> throwIO (ProtocolError "unexpected bulk unit-result shape")+  where+    ordinary (PreparedOK _) = True+    ordinary (PreparedRows columns _) = length columns == 2+    ordinary _ = False+    unit [ident, affected] = BulkUnitResult <$> unsigned ident <*> unsigned affected+    unit _ = throwIO (ProtocolError "invalid bulk unit-result row")+    -- The server describes these as BIGINT and stores their unsigned bit pattern.+    unsigned (MySQLInt64U value) = pure value+    unsigned (MySQLInt64 value) = pure (fromIntegral value)+    unsigned _ = throwIO (ProtocolError "invalid bulk unit-result integer")++-- No exception thrown by cleanup replaces an ordinary application failure.+-- As with statement cleanup, restore only the timed I/O. The lease and failure+-- diagnostics remain protected while buffered draining is interruptible.+cleanupTransaction :: (IO () -> IO ()) -> MySQLConn -> IO ()+cleanupTransaction restore conn = do+    phase <- connectionStatus conn+    when (phase `elem` [Ready,Streaming]) $ do+        cleaned <- timeout (cleanupTimeoutMicros (connectionOptions conn)) $ restore $ do+            pending <- readIORef (connectionDrain conn)+            maybe (pure ()) id pending+            void (execute_ conn "ROLLBACK")+        when (cleaned==Nothing) (abortConnection conn False >> throwIO CleanupDeadlineExceeded)++-- | Own the connection across BEGIN, the body and completion. Nested/manual+-- transactions are rejected. Unread rows or a body exception trigger draining+-- and rollback within one cleanup deadline; cleanup preserves the primary+-- exception unless a new cancellation arrives.+--+-- Lost COMMIT acknowledgement raises 'CommitOutcomeUnknown' for synchronous+-- failures. Cancellation is rethrown unchanged and uncertainty is recorded in+-- 'connectionFailure'. The helper never retries COMMIT or replays the body.+withTransaction :: MySQLConn -> IO a -> IO a+withTransaction conn body = mask $ \restore -> do+    beginTransactionLease conn+    let run = do+            guardUnconsumed conn+            flags <- readIORef (connectionServerStatus conn)+            active <- case flags of+                Just value -> pure (value .&. 1 /= 0)+                Nothing -> do+                    (_,stream) <- query_ conn "SELECT @@in_transaction"+                    values <- Stream.toList stream+                    pure (values `notElem` [[[MySQLInt64 0]],[[MySQLInt8 0]],[[MySQLInt32 0]],[[MySQLInt64U 0]],[[MySQLInt8U 0]],[[MySQLInt32U 0]]])+            when active (throwIO NestedTransactionUnsupported)+            void (execute_ conn "BEGIN")+            outcome <- try (restore body >>= \value -> guardUnconsumed conn >> pure value)+            case outcome of+                Left (primary :: SomeException) -> do+                    cleanupTransaction restore conn `catch` \(secondary :: SomeException) -> do+                        abortConnection conn False+                        atomicModifyIORef' (connectionFailureRef conn) (\failure -> (fmap (\f -> f {secondaryCleanupFailure=True}) failure,()))+                        when (isCancellation secondary) (throwIO secondary)+                    throwIO primary+                Right value -> do+                    commitGeneration <- runtimeGeneration <$> readIORef (connectionRuntime conn)+                    _ <- execute_ conn "COMMIT" `catch` \(err :: SomeException) -> do+                        runtime <- readIORef (connectionRuntime conn)+                        progress <- readIORef (connectionExchangeProgress conn)+                        if runtimeStatus runtime `elem` [Broken,Closed] && runtimeGeneration runtime/=commitGeneration && progress==WireInFlight then do+                            atomicModifyIORef' (connectionFailureRef conn) (\failure ->+                                let marked=case failure of Nothing -> ConnectionFailure Executing True False+                                                           Just previous -> previous {commitOutcomeUncertain=True}+                                in (Just marked,()))+                            if isCancellation err then throwIO err else throwIO (CommitOutcomeUnknown err)+                        else throwIO err+                    pure value+    run `finally` endTransactionLease conn
+ src/Database/MySQL/BinLog.hs view
@@ -0,0 +1,398 @@+{-|+Module      : Database.MySQL.BinLog+Description : MariaDB CDC, GTID resume and controlled acknowledgements+Copyright   : (c) Winterland, 2016+License     : BSD+Maintainer  : Anton Antich <anton.antich@gmail.com>+Stability   : experimental+Portability : PORTABLE++Use 'openCDC' for a bounded, pull-driven row-based change stream. Configure+MariaDB binary logging, row format, retention and replication grants separately.+Choose a unique nonzero replica ID and dedicate one connection/thread to the+stream. Stream termination or failure closes/retires that connection.++'FromGtid' uses MariaDB @domain-server-sequence@ state, not UUID intervals.+Checkpoints describe completed decoded groups; they prove no destination+durability. Apply events and atomically persist destination changes with the+exported resume state before 'cdcAcknowledge'. The driver supplies neither+persistence nor an exactly-once destination protocol. Manual ACKs require+server semisync; legacy 'dumpBinLog' on-read ACKs mean receipt only.++Persist 'checkpointPosition' or 'checkpointGtids', never an opaque token's+@Show@ representation. Reopen a fresh connection after interruption and resume+from durable coordinates. Table metadata/event expansion are bounded; the+consumer must also avoid buffering unbounded transaction groups.++Advanced XA checkpointing, two-phase ALTER and storage-engine-binlog GTID+semisync are unsupported. Unknown non-ignorable events fail closed. Traditional+file dump starts have a 32-bit wire limit even though checkpoint positions are+64-bit. See the+<https://github.com/aantich/mariadb-haskell/blob/main/docs/USER_GUIDE.md CDC user guide>+and <https://github.com/aantich/mariadb-haskell/blob/main/examples/GuideExamples.hs consumer example>.+-}++module Database.MySQL.BinLog+    ( -- * binlog utilities+      SlaveID+    , BinLogTracker(..)+    , registerPesudoSlave+    , dumpBinLog+    , BinLogStart(..), AcknowledgementMode(..), CDCOptions(..), defaultCDCOptions+    , CDCStream(..), openCDC, resumeFromCheckpoint+    , module Database.MySQL.BinLogProtocol.CDC+    , RowBinLogEvent(..)+    , decodeRowBinLogEvent+    -- * helpers+    , getLastBinLogTracker+    , isCheckSumEnabled+    , isSemiSyncEnabled+    -- * re-export+    , module Database.MySQL.BinLogProtocol.BinLogEvent+    , module Database.MySQL.BinLogProtocol.BinLogValue+    , module Database.MySQL.BinLogProtocol.BinLogMeta+    ) where++import           Control.Exception                         (throwIO, mask, mask_, onException, finally)+import           Control.Monad+import           Data.ByteString                           (ByteString)+import           Data.Bits+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as L+import           Database.MySQL.BinLogProtocol.CDC+import           Data.IORef                                (IORef, newIORef,+                                                            readIORef,+                                                            writeIORef)+import           Data.Text.Encoding                        (encodeUtf8)+import           Data.Word+import           Database.MySQL.Base+import           Database.MySQL.BinLogProtocol.BinLogEvent+import           Database.MySQL.BinLogProtocol.BinLogMeta+import           Database.MySQL.BinLogProtocol.BinLogValue+import           Database.MySQL.Connection+import           GHC.Generics                              (Generic)+import           System.IO.Streams                         (InputStream)+import qualified System.IO.Streams                         as Stream++-- | Unique nonzero replica server ID chosen by the application.+type SlaveID = Word32++-- | File-based start coordinate. The next-position field is 64-bit to preserve+-- observed positions; opening the legacy dump command rejects values over its+-- 32-bit start limit rather than truncating them.+data BinLogTracker = BinLogTracker+    { btFileName :: {-# UNPACK #-} !ByteString+    , btNextPos  :: {-# UNPACK #-} !Word64+    } deriving (Show, Eq, Generic)++-- | Register a replica with MariaDB using COM_REGISTER_SLAVE. The historical+-- function name is retained for API compatibility. Registration is optional+-- for a dump connection; choose a unique nonzero replica ID either way.+registerPesudoSlave :: MySQLConn -> SlaveID -> IO OK+registerPesudoSlave conn sid = command conn (COM_REGISTER_SLAVE sid "" "" "" 0 0 0)++-- | Dedicate the connection to binlog listening. Consume the returned stream on+-- the calling thread; other commands are rejected while it is 'Replicating'.+-- Setup/stream failures retire the connection. End-of-stream closes it, because+-- replication has a distinct packet-sequence policy from ordinary commands.+--+dumpBinLog :: MySQLConn               -- ^ connection to be listened+           -> SlaveID                 -- ^ a number for our pesudo slave.+           -> BinLogTracker           -- ^ binlog position+           -> Bool                    -- ^ if master support semi-ack, do we want to enable it?+                                      -- if master doesn't support, this parameter will be ignored.+           -> IO (FormatDescription, IORef ByteString, InputStream BinLogPacket)+                -- ^ t'FormatDescription', 'IORef' contains current binlog filename, t'BinLogPacket' stream.+dumpBinLog conn sid tracker wantAck = do+    let mode = if wantAck then AcknowledgeOnRead else AcknowledgementsDisabled+    (fmt, ref, packets, _) <- openPackets conn sid+        (defaultCDCOptions (FromBinLog tracker)) { cdcAcknowledgements = mode }+    legacy <- Stream.makeInputStream $ do+        value <- Stream.read packets+        case value of+            Nothing -> close conn >> pure Nothing+            Just event -> pure (Just event)+    pure (fmt, ref, legacy)++-- | Explicit start position. GTID sets contain only previously completed+-- transactions; the server selects the next available group, including across+-- file rotations. Purged history is an error, never a silent fallback.+data BinLogStart = FromBinLog !BinLogTracker | FromGtid !GtidSet deriving (Eq, Show)++-- | Manual mode never acknowledges during reading. The legacy on-read mode+-- acknowledges receipt only and must not be used as a durability guarantee.+data AcknowledgementMode = AcknowledgementsDisabled | AcknowledgeManually | AcknowledgeOnRead+    deriving (Eq, Show)++-- | Replication setup and decoding bounds. Nonblocking mode finishes at the+-- server's current log end; ordinary blocking streams wait for further work.+data CDCOptions = CDCOptions+    { cdcStart :: !BinLogStart+      -- ^ Proven file coordinate or completed MariaDB GTID state.+    , cdcAcknowledgements :: !AcknowledgementMode+      -- ^ Disabled by default. 'openCDC' accepts manual or disabled mode only.+    , cdcNonBlocking :: !Bool+      -- ^ End at the server's current log end instead of waiting for new events.+    , cdcDecodeLimits :: !DecodeLimits+      -- ^ Independent retained-table-metadata and event-expansion budgets.+    , cdcBootstrapEventLimit :: !Int+      -- ^ Maximum setup events before format/checksum state is established.+    } deriving (Eq, Show)++-- | Blocking, no acknowledgements, bounded decoding and at most 1024 setup+-- events. Register a unique nonzero replica server ID at the application level.+defaultCDCOptions :: BinLogStart -> CDCOptions+defaultCDCOptions start = CDCOptions start AcknowledgementsDisabled False defaultDecodeLimits 1024++-- | Choose the proven resume mechanism for a completed checkpoint. File-started+-- streams retain file coordinates; GTID-started streams retain their full domain+-- state. Call 'openCDC' on a fresh connection; this helper performs no I/O.+resumeFromCheckpoint :: CDCCheckpoint -> BinLogStart+resumeFromCheckpoint token = case checkpointGtids token of+    Just gtids -> FromGtid gtids+    Nothing -> let position = checkpointPosition token+               in FromBinLog (BinLogTracker (binlogFile position) (binlogNextPosition position))++-- | Single-thread-owned change stream. 'cdcLatestCheckpoint' observes a+-- decoded complete-group boundary while the stream is active, not application durability. Persist it+-- atomically with destination changes before calling 'cdcAcknowledge'. Closing+-- is terminal; reopening/resuming always uses a fresh connection.+data CDCStream = CDCStream+    { cdcEvents :: !(InputStream CDCEvent)+      -- ^ Consume once on the opening thread; backpressure is controlled by reads.+    , cdcLatestCheckpoint :: !(IO (Maybe CDCCheckpoint))+      -- ^ Latest complete decoded group, independent of destination durability.+    , cdcAcknowledge :: !(CDCCheckpoint -> IO ())+      -- ^ Acknowledge a durable active-stream checkpoint; never an arbitrary token.+    , cdcClose :: !(IO ())+      -- ^ Terminal close of the dedicated connection, also used by brackets.+    }++-- | Dedicate a connection to MariaDB CDC. Stream decoding and acknowledgement+-- failures retire the transport. Cancellation never retries SQL or advances a+-- checkpoint. Manual acknowledgements require server semisync to be enabled;+-- acknowledgement tokens belong to this stream and cannot refer to future or+-- incomplete transactions. Semisync acknowledges replica receipt, not an+-- exactly-once destination commit protocol.+openCDC :: MySQLConn -> SlaveID -> CDCOptions -> IO CDCStream+openCDC conn sid options = mask $ \restore -> do+    when (cdcAcknowledgements options == AcknowledgeOnRead)+        (throwIO (CDCInvalidConfiguration "openCDC requires manual or disabled acknowledgements"))+    (fd, ref, packets, generation) <- restore (openPackets conn sid options)+    initialFile <- readIORef ref+    let initialGtids = case cdcStart options of+            FromGtid entries -> Just entries+            FromBinLog _ -> Nothing+    (decoded, latest) <- newCDCDecoder (cdcDecodeLimits options) initialGtids fd initialFile packets+        `onException` abortConnection conn False+    finished <- newIORef False+    guarded <- Stream.makeInputStream $ do+        done <- readIORef finished+        if done then pure Nothing else streamGuard conn generation $ do+            value <- Stream.read decoded+            case value of+                Nothing -> writeIORef finished True >> close conn >> pure Nothing+                Just event -> pure (Just event)+    acknowledged <- newIORef Nothing+    let acknowledge token = do+            streamGuard conn generation (pure ())+            -- Invalid tokens are local misuse: reject without poisoning a+            -- healthy replication session or transmitting bytes.+            unless (cdcAcknowledgements options == AcknowledgeManually)+                (throwIO (CDCInvalidCheckpoint "manual acknowledgements are disabled"))+            current <- latest+            unless (maybe False (`validCheckpoint` token) current)+                (throwIO (CDCInvalidCheckpoint "checkpoint does not belong to delivered stream history"))+            prior <- readIORef acknowledged+            case prior of+                Just previous | previous == token -> pure ()+                Just previous | not (validCheckpoint token previous) ->+                    throwIO (CDCInvalidCheckpoint "checkpoint would move acknowledgement backwards")+                _ -> streamGuard conn generation $ mask_ $ do+                    let position = checkpointPosition token+                    sendSemiAck conn (binlogNextPosition position) (binlogFile position)+                    writeIORef acknowledged (Just token)+    pure (CDCStream guarded (streamGuard conn generation latest) acknowledge (close conn))++-- Semisync ACKs are independent no-reply exchanges, even when compressed.+-- Reset only the outgoing frame counter: buffered incoming bytes must survive.+-- openPackets installs the replication receive policy before reading events.+sendSemiAck :: MySQLConn -> Word64 -> ByteString -> IO ()+sendSemiAck conn position file = do+    connectionCompressionReset conn+    mysqlWrite conn (putToPacket 0 (putSemiAckResp position file))++-- Both legacy and transaction-aware interfaces use the same checked wire+-- reader. The low capability value deliberately avoids advertising MariaDB+-- storage-engine-binlog/GTID semisync extensions that this file-position ACK+-- implementation does not support.+openPackets :: MySQLConn -> SlaveID -> CDCOptions+            -> IO (FormatDescription, IORef ByteString, InputStream BinLogPacket, Word64)+openPackets conn sid options = do+    let limits = cdcDecodeLimits options+    when (sid == 0 || cdcBootstrapEventLimit options <= 0 || maxTableMaps limits <= 0+          || maxTableMapBytes limits <= 0 || maxExpandedEventBytes limits <= 0)+        (throwIO (CDCInvalidConfiguration "invalid CDC server ID/resource limits"))+    (initialFile, initialPosition) <- case cdcStart options of+        FromBinLog (BinLogTracker file position) -> do+            when (B.null file || B.length file > 512 || B.elem 0 file || position < 4+                  || position > fromIntegral (maxBound :: Word32))+                (throwIO (CDCInvalidConfiguration "invalid/out-of-range COM_BINLOG_DUMP position"))+            pure (file, fromIntegral position)+        FromGtid entries -> do+            when (B.length (renderGtidSet entries) > 65535)+                (throwIO (CDCInvalidConfiguration "GTID connect state is too large"))+            pure ("", 4)+    mask $ \restore -> do+        guardUnconsumed conn+        beginTransactionLease conn+        (restore (setup initialFile initialPosition limits) `onException` abortConnection conn False)+            `finally` endTransactionLease conn+  where+    setup initialFile initialPosition limits = do+        semi <- if cdcAcknowledgements options == AcknowledgementsDisabled then pure False else isSemiSyncEnabled conn+        when (cdcAcknowledgements options == AcknowledgeManually && not semi)+            (throwIO (CDCUnsupportedFeature "manual ACK requested but MariaDB semisync is not enabled"))+        let needAck = semi && cdcAcknowledgements options /= AcknowledgementsDisabled+        -- One SET restores all replication setup variables atomically. GTID+        -- text is generated exclusively from validated numeric identities, so+        -- the SQL is ASCII-only and cannot depend on an unknown client charset+        -- or SQL mode. No user-provided SQL bytes are interpolated here.+        let connectState = case cdcStart options of+                FromGtid entries -> "'" <> renderGtidSet entries <> "'"+                FromBinLog _ -> "NULL"+            setupSQL = "SET @mariadb_slave_capability=4,@slave_gtid_strict_mode=1,@slave_gtid_ignore_duplicates=0,@slave_connect_state="+                <> connectState <> ",@master_binlog_checksum='CRC32',@rpl_semi_sync_slave=" <> if needAck then "1" else "0"+        void $ execute_ conn (Query (L.fromStrict setupSQL))+        -- Declaring CRC32 support also makes the artificial initial ROTATE+        -- checksummed. Subsequent files choose their mode in their own FDE,+        -- which can differ from the server's current global setting.+        checksumState <- newIORef True+        -- Apply the CDC ceiling while assembling a logical message, so an+        -- oversized first fragment cannot make us read more continuations.+        -- Word64 arithmetic also permits maxBound Int without signed overflow.+        -- Physical bodies remain bounded before reading by ConnectionOptions.+        let wireLimit = min (maximumMessageBytes (connectionOptions conn))+                (fromIntegral (maxExpandedEventBytes limits) + 1024)+        withCommand conn $ do+            let flags = (if cdcNonBlocking options then 1 else 0) .|. 2+            sendCommand conn (COM_BINLOG_DUMP initialPosition flags sid initialFile)+            generation <- markResponseStream conn Replicating+            setExchangeSequence conn 256+            when needAck (connectionCompressionAllowReplicationResets conn)+            let readEvent = do+                    packet <- readPacketWithLimit wireLimit (mysqlRead conn)+                    if | isOK packet -> do+                           checksum <- readIORef checksumState+                           event <- getFromPacket (getBinLogPacket checksum needAck) packet+                           when (blEventType event == BINLOG_FORMAT_DESCRIPTION_EVENT) $ do+                               -- FDE always carries a CRC, even when its+                               -- descriptor turns checksums off for this file.+                               void (getFromBinLogPacket getFormatDescription event)+                               writeIORef checksumState (L.last (blBody event) == 1)+                           pure (Just event)+                       | isEOF packet -> pure Nothing+                       | isERR packet -> decodeFromPacket packet >>= throwIO . ERRException+                       | otherwise -> throwIO (UnexpectedPacket packet)+                findType remaining typ = do+                    when (remaining <= 0) (throwIO (CDCResourceLimit "CDC bootstrap event limit exceeded"))+                    packet <- readEvent+                    case packet of+                        Nothing -> throwIO NetworkException+                        Just value | blEventType value == typ -> pure value+                                   | blEventType value `elem` [BINLOG_HEARTBEAT_EVENT,BINLOG_IGNORABLE_EVENT]+                                   || blFlags value .&. 0x80 /= 0 -> findType (remaining - 1) typ+                               | otherwise -> throwIO (CDCDecodeError "unexpected data event during CDC bootstrap")+            rotationPacket <- findType (cdcBootstrapEventLimit options) BINLOG_ROTATE_EVENT+            rotation <- getFromBinLogPacket getRotateEvent rotationPacket+            ref <- newIORef (B.copy (rFileName rotation))+            formatPacket <- findType (cdcBootstrapEventLimit options) BINLOG_FORMAT_DESCRIPTION_EVENT+            fd <- getFromBinLogPacket getFormatDescription formatPacket+            let autoAck packet = when (needAck && blSemiAck packet && cdcAcknowledgements options == AcknowledgeOnRead) $ do+                    file <- readIORef ref+                    sendSemiAck conn (blLogPos packet) file+            autoAck formatPacket+            finished <- newIORef False+            packets <- Stream.makeInputStream $ do+                done <- readIORef finished+                if done then pure Nothing else streamGuard conn generation $ do+                    next <- readEvent+                    case next of+                        Nothing -> writeIORef finished True >> pure Nothing+                        Just event -> do+                            when (blEventType event == BINLOG_ROTATE_EVENT) $ do+                                value <- getFromBinLogPacket getRotateEvent event+                                writeIORef ref (B.copy (rFileName value))+                            autoAck event+                            pure (Just event)+            pure (fd, ref, packets, generation)++-- | Legacy row-only view. Trackers after individual rows are not transaction+-- checkpoints: persisting one may resume in the middle of a transaction.+-- Use 'openCDC' for transaction boundaries, MariaDB annotations and safe resume.+-- The first field is the server event timestamp in seconds.+data RowBinLogEvent+    = RowQueryEvent  {-# UNPACK #-} !Word32 !BinLogTracker !QueryEvent'+    | RowDeleteEvent {-# UNPACK #-} !Word32 !BinLogTracker !TableMapEvent !DeleteRowsEvent+    | RowWriteEvent  {-# UNPACK #-} !Word32 !BinLogTracker !TableMapEvent !WriteRowsEvent+    | RowUpdateEvent {-# UNPACK #-} !Word32 !BinLogTracker !TableMapEvent !UpdateRowsEvent+  deriving (Show, Eq, Generic)++-- | Decode the legacy row-only view with bounded table-ID metadata. The input+-- is single-consumer. Bracket the connection lifetime: semantic decoding errors+-- propagate from this offline adapter, which has no connection to retire.+-- Byte-valued row fields may share event storage; copy small retained fields+-- when keeping them beyond consumption of their containing event.+decodeRowBinLogEvent :: (FormatDescription, IORef ByteString, InputStream BinLogPacket)+                     -> IO (InputStream RowBinLogEvent)+decodeRowBinLogEvent (fd, ref, input) = do+    file <- readIORef ref+    (events, _) <- newCDCDecoder defaultDecodeLimits Nothing fd file input+    let next = do+            event <- Stream.read events+            case event of+                Nothing -> pure Nothing+                Just value -> do+                    let BinLogPosition name position = cdcEventPosition value+                        tracker = BinLogTracker name position+                        stamp = cdcEventTimestamp value+                    case cdcEventPayload value of+                        CDCRowQuery rowQuery -> pure (Just (RowQueryEvent stamp tracker rowQuery))+                        CDCWrite table row -> pure (Just (RowWriteEvent stamp tracker table row))+                        CDCUpdate table row -> pure (Just (RowUpdateEvent stamp tracker table row))+                        CDCDelete table row -> pure (Just (RowDeleteEvent stamp tracker table row))+                        _ -> next+    Stream.makeInputStream next++-- | Get latest master's binlog filename and position.+--+getLastBinLogTracker :: MySQLConn -> IO (Maybe BinLogTracker)+getLastBinLogTracker conn = do+    (_, is) <- query_ conn "SHOW MASTER STATUS"+    row <- Stream.read is+    Stream.skipToEof is+    case row of+        Just (MySQLText fn : MySQLInt64U pos : _) -> return . Just $ BinLogTracker (encodeUtf8 fn) pos+        _                                         -> return Nothing++-- | Return True if binlog_checksum = CRC32. MariaDB CRC32 mode+--+isCheckSumEnabled :: MySQLConn -> IO Bool+isCheckSumEnabled conn = do+    (_, is) <- query_ conn "SHOW GLOBAL VARIABLES LIKE 'binlog_checksum'"+    row <- Stream.read is+    Stream.skipToEof is+    case row of+        Just [_, MySQLText "CRC32"] -> return True+        _                           -> return False++-- | Return True if rpl_semi_sync_master_enabled = ON. MariaDB semisynchronous replication+--+isSemiSyncEnabled :: MySQLConn -> IO Bool+isSemiSyncEnabled conn = do+    (_, is) <- query_ conn "SHOW VARIABLES LIKE 'rpl_semi_sync_master_enabled'"+    row <- Stream.read is+    Stream.skipToEof is+    case row of+        Just [_, MySQLText "ON"] -> return True+        _                        -> return False
+ src/Database/MySQL/BinLogProtocol/BinLogEvent.hs view
@@ -0,0 +1,568 @@+{-# OPTIONS_GHC -funbox-strict-fields #-}++{-|+Module      : Database.MySQL.BinLogProtocol.BinLogEvent+Description : Binlog event+Copyright   : (c) Winterland, 2016+License     : BSD+Maintainer  : Anton Antich <anton.antich@gmail.com>+Stability   : experimental+Portability : PORTABLE++Binlog event type+-}++module Database.MySQL.BinLogProtocol.BinLogEvent+    ( BinLogEventType(..), BinLogPacket(..), putSemiAckResp, getBinLogPacket+    , binLogCRC32, getFromBinLogPacket, getFromBinLogPacket'+    , FormatDescription(..), getFormatDescription, eventHeaderLen+    , RotateEvent(..), getRotateEvent, QueryEvent(..), getQueryEvent+    , QueryEvent'(..), getQueryEvent', TableMapEvent(..), getTableMapEvent+    , DeleteRowsEvent(..), getDeleteRowEvent, WriteRowsEvent(..), getWriteRowEvent+    , UpdateRowsEvent(..), getUpdateRowEvent, getPresentMap+    ) where++import           Control.Monad+import qualified Data.Vector.Unboxed                       as V+import           Data.Binary+import           Data.Binary.Put (putWord64le, putByteString)+import           Data.Binary.Parser+import           Data.Bits+import           Data.ByteString                           (ByteString)+import qualified Data.ByteString                           as B+import qualified Data.ByteString.Lazy                      as L+import qualified Data.ByteString.Unsafe                    as B+import           Database.MySQL.BinLogProtocol.BinLogMeta+import           Database.MySQL.BinLogProtocol.BinLogValue+import           Database.MySQL.Protocol.Packet+import           Database.MySQL.Protocol.MySQLValue+import           Database.MySQL.Protocol.ColumnDef++import           Control.Exception                         (throwIO)+import           Database.MySQL.Query+import           GHC.Generics                              (Generic)++--------------------------------------------------------------------------------+-- | Binlog event identifiers, including MariaDB extensions.+--+data BinLogEventType+    = BINLOG_UNKNOWN_EVENT+    | BINLOG_START_EVENT_V3+    | BINLOG_QUERY_EVENT+    | BINLOG_STOP_EVENT+    | BINLOG_ROTATE_EVENT+    | BINLOG_INTVAR_EVENT+    | BINLOG_LOAD_EVENT+    | BINLOG_SLAVE_EVENT+    | BINLOG_CREATE_FILE_EVENT+    | BINLOG_APPEND_BLOCK_EVENT+    | BINLOG_EXEC_LOAD_EVENT+    | BINLOG_DELETE_FILE_EVENT+    | BINLOG_NEW_LOAD_EVENT+    | BINLOG_RAND_EVENT+    | BINLOG_USER_VAR_EVENT+    | BINLOG_FORMAT_DESCRIPTION_EVENT+    | BINLOG_XID_EVENT+    | BINLOG_BEGIN_LOAD_QUERY_EVENT+    | BINLOG_EXECUTE_LOAD_QUERY_EVENT+    | BINLOG_TABLE_MAP_EVENT+    | BINLOG_WRITE_ROWS_EVENTv0+    | BINLOG_UPDATE_ROWS_EVENTv0+    | BINLOG_DELETE_ROWS_EVENTv0+    | BINLOG_WRITE_ROWS_EVENTv1+    | BINLOG_UPDATE_ROWS_EVENTv1+    | BINLOG_DELETE_ROWS_EVENTv1+    | BINLOG_INCIDENT_EVENT+    | BINLOG_HEARTBEAT_EVENT+    | BINLOG_IGNORABLE_EVENT+    | BINLOG_ROWS_QUERY_EVENT+    | BINLOG_WRITE_ROWS_EVENTv2+    | BINLOG_UPDATE_ROWS_EVENTv2+    | BINLOG_DELETE_ROWS_EVENTv2+    | BINLOG_GTID_EVENT+    | BINLOG_ANONYMOUS_GTID_EVENT+    | BINLOG_PREVIOUS_GTIDS_EVENT+    | MARIADB_ANNOTATE_ROWS_EVENT+    | MARIADB_BINLOG_CHECKPOINT_EVENT+    | MARIADB_GTID_EVENT+    | MARIADB_GTID_LIST_EVENT+    | MARIADB_START_ENCRYPTION_EVENT+    | MARIADB_QUERY_COMPRESSED_EVENT+    | MARIADB_WRITE_ROWS_COMPRESSED_EVENTv1+    | MARIADB_UPDATE_ROWS_COMPRESSED_EVENTv1+    | MARIADB_DELETE_ROWS_COMPRESSED_EVENTv1+    | MARIADB_WRITE_ROWS_COMPRESSED_EVENTv2+    | MARIADB_UPDATE_ROWS_COMPRESSED_EVENTv2+    | MARIADB_DELETE_ROWS_COMPRESSED_EVENTv2+    | MARIADB_PARTIAL_ROW_DATA_EVENT+    | UnrecognizedBinLogEvent !Word8+  deriving (Show, Eq)++-- | Preserve wire identifiers, including future events. Unlike derived Enum,+-- decoding a new MariaDB event can never raise a pure out-of-range exception.+instance Enum BinLogEventType where+    toEnum n = case n of+        0 -> BINLOG_UNKNOWN_EVENT+        1 -> BINLOG_START_EVENT_V3+        2 -> BINLOG_QUERY_EVENT+        3 -> BINLOG_STOP_EVENT+        4 -> BINLOG_ROTATE_EVENT+        5 -> BINLOG_INTVAR_EVENT+        6 -> BINLOG_LOAD_EVENT+        7 -> BINLOG_SLAVE_EVENT+        8 -> BINLOG_CREATE_FILE_EVENT+        9 -> BINLOG_APPEND_BLOCK_EVENT+        10 -> BINLOG_EXEC_LOAD_EVENT+        11 -> BINLOG_DELETE_FILE_EVENT+        12 -> BINLOG_NEW_LOAD_EVENT+        13 -> BINLOG_RAND_EVENT+        14 -> BINLOG_USER_VAR_EVENT+        15 -> BINLOG_FORMAT_DESCRIPTION_EVENT+        16 -> BINLOG_XID_EVENT+        17 -> BINLOG_BEGIN_LOAD_QUERY_EVENT+        18 -> BINLOG_EXECUTE_LOAD_QUERY_EVENT+        19 -> BINLOG_TABLE_MAP_EVENT+        20 -> BINLOG_WRITE_ROWS_EVENTv0+        21 -> BINLOG_UPDATE_ROWS_EVENTv0+        22 -> BINLOG_DELETE_ROWS_EVENTv0+        23 -> BINLOG_WRITE_ROWS_EVENTv1+        24 -> BINLOG_UPDATE_ROWS_EVENTv1+        25 -> BINLOG_DELETE_ROWS_EVENTv1+        26 -> BINLOG_INCIDENT_EVENT+        27 -> BINLOG_HEARTBEAT_EVENT+        28 -> BINLOG_IGNORABLE_EVENT+        29 -> BINLOG_ROWS_QUERY_EVENT+        30 -> BINLOG_WRITE_ROWS_EVENTv2+        31 -> BINLOG_UPDATE_ROWS_EVENTv2+        32 -> BINLOG_DELETE_ROWS_EVENTv2+        33 -> BINLOG_GTID_EVENT+        34 -> BINLOG_ANONYMOUS_GTID_EVENT+        35 -> BINLOG_PREVIOUS_GTIDS_EVENT+        160 -> MARIADB_ANNOTATE_ROWS_EVENT+        161 -> MARIADB_BINLOG_CHECKPOINT_EVENT+        162 -> MARIADB_GTID_EVENT+        163 -> MARIADB_GTID_LIST_EVENT+        164 -> MARIADB_START_ENCRYPTION_EVENT+        165 -> MARIADB_QUERY_COMPRESSED_EVENT+        166 -> MARIADB_WRITE_ROWS_COMPRESSED_EVENTv1+        167 -> MARIADB_UPDATE_ROWS_COMPRESSED_EVENTv1+        168 -> MARIADB_DELETE_ROWS_COMPRESSED_EVENTv1+        169 -> MARIADB_WRITE_ROWS_COMPRESSED_EVENTv2+        170 -> MARIADB_UPDATE_ROWS_COMPRESSED_EVENTv2+        171 -> MARIADB_DELETE_ROWS_COMPRESSED_EVENTv2+        172 -> MARIADB_PARTIAL_ROW_DATA_EVENT+        _ -> UnrecognizedBinLogEvent (fromIntegral n)+    fromEnum t = case t of+        BINLOG_UNKNOWN_EVENT -> 0+        BINLOG_START_EVENT_V3 -> 1+        BINLOG_QUERY_EVENT -> 2+        BINLOG_STOP_EVENT -> 3+        BINLOG_ROTATE_EVENT -> 4+        BINLOG_INTVAR_EVENT -> 5+        BINLOG_LOAD_EVENT -> 6+        BINLOG_SLAVE_EVENT -> 7+        BINLOG_CREATE_FILE_EVENT -> 8+        BINLOG_APPEND_BLOCK_EVENT -> 9+        BINLOG_EXEC_LOAD_EVENT -> 10+        BINLOG_DELETE_FILE_EVENT -> 11+        BINLOG_NEW_LOAD_EVENT -> 12+        BINLOG_RAND_EVENT -> 13+        BINLOG_USER_VAR_EVENT -> 14+        BINLOG_FORMAT_DESCRIPTION_EVENT -> 15+        BINLOG_XID_EVENT -> 16+        BINLOG_BEGIN_LOAD_QUERY_EVENT -> 17+        BINLOG_EXECUTE_LOAD_QUERY_EVENT -> 18+        BINLOG_TABLE_MAP_EVENT -> 19+        BINLOG_WRITE_ROWS_EVENTv0 -> 20+        BINLOG_UPDATE_ROWS_EVENTv0 -> 21+        BINLOG_DELETE_ROWS_EVENTv0 -> 22+        BINLOG_WRITE_ROWS_EVENTv1 -> 23+        BINLOG_UPDATE_ROWS_EVENTv1 -> 24+        BINLOG_DELETE_ROWS_EVENTv1 -> 25+        BINLOG_INCIDENT_EVENT -> 26+        BINLOG_HEARTBEAT_EVENT -> 27+        BINLOG_IGNORABLE_EVENT -> 28+        BINLOG_ROWS_QUERY_EVENT -> 29+        BINLOG_WRITE_ROWS_EVENTv2 -> 30+        BINLOG_UPDATE_ROWS_EVENTv2 -> 31+        BINLOG_DELETE_ROWS_EVENTv2 -> 32+        BINLOG_GTID_EVENT -> 33+        BINLOG_ANONYMOUS_GTID_EVENT -> 34+        BINLOG_PREVIOUS_GTIDS_EVENT -> 35+        MARIADB_ANNOTATE_ROWS_EVENT -> 160+        MARIADB_BINLOG_CHECKPOINT_EVENT -> 161+        MARIADB_GTID_EVENT -> 162+        MARIADB_GTID_LIST_EVENT -> 163+        MARIADB_START_ENCRYPTION_EVENT -> 164+        MARIADB_QUERY_COMPRESSED_EVENT -> 165+        MARIADB_WRITE_ROWS_COMPRESSED_EVENTv1 -> 166+        MARIADB_UPDATE_ROWS_COMPRESSED_EVENTv1 -> 167+        MARIADB_DELETE_ROWS_COMPRESSED_EVENTv1 -> 168+        MARIADB_WRITE_ROWS_COMPRESSED_EVENTv2 -> 169+        MARIADB_UPDATE_ROWS_COMPRESSED_EVENTv2 -> 170+        MARIADB_DELETE_ROWS_COMPRESSED_EVENTv2 -> 171+        MARIADB_PARTIAL_ROW_DATA_EVENT -> 172+        UnrecognizedBinLogEvent n -> fromIntegral n++data BinLogPacket = BinLogPacket+    { blTimestamp :: !Word32+    , blEventType :: !BinLogEventType+    , blServerId  :: !Word32+    , blEventSize :: !Word32+    , blLogPos    :: !Word64   -- ^ for future GTID compatibility+    , blFlags     :: !Word16+    , blBody      :: !L.ByteString+    , blSemiAck   :: !Bool+    } deriving (Show, Eq)++-- | Semisynchronous acknowledgement payload: marker, little-endian 64-bit+-- position, then the un-terminated filename (MariaDB semisync wire format).+putSemiAckResp :: Word64 -> ByteString -> Put+putSemiAckResp pos fn = putWord8 0xef >> putWord64le pos >> putByteString fn++-- | Decode exactly one replication message, verifying CRC32 over the complete+-- event header and body. Packet framing bytes and semisync headers are excluded.+getBinLogPacket :: Bool -> Bool -> Get BinLogPacket+getBinLogPacket checksum semi = do+    marker <- getWord8+    unless (marker == 0) (fail "binlog: missing OK marker")+    ack <- if semi then do+        magic <- getWord8+        flags <- getWord8+        unless (magic == 0xef && flags .&. complement 1 == 0)+            (fail "binlog: malformed semisync header")+        pure (flags == 1)+      else pure False+    raw <- getRemainingLazyByteString+    let hasChecksum = checksum || (L.length raw >= 5 && L.index raw 4 == 15)+        trailer = if hasChecksum then 4 else 0+        total = L.length raw+    when (total < 19 + trailer) (fail "binlog: truncated common header/checksum")+    let parser = do+            ts <- getWord32le+            typ <- toEnum . fromIntegral <$> getWord8+            sid <- getWord32le+            size <- getWord32le+            pos <- getWord32le+            flags <- getWord16le+            unless (fromIntegral size == total) (fail "binlog: event size disagrees with message")+            body <- getLazyByteString (total - 19 - trailer)+            when hasChecksum $ do+                expected <- getWord32le+                unless (expected == binLogCRC32 (L.take (total - 4) raw))+                    (fail "binlog: CRC32 mismatch")+            pure (BinLogPacket ts typ sid size (fromIntegral pos) flags body ack)+    case runGetOrFail parser raw of+        Left (_, _, message) -> fail message+        Right (_, _, event) -> pure event++-- | IEEE CRC32 used by MariaDB. A shared unboxed lookup table avoids eight+-- branches per payload byte and does not concatenate lazy event chunks.+binLogCRC32 :: L.ByteString -> Word32+binLogCRC32 = complement . L.foldl' step 0xffffffff+  where+    step crc byte = (crc `shiftR` 8) `xor`+        (crcTable V.! fromIntegral ((crc `xor` fromIntegral byte) .&. 255))++crcTable :: V.Vector Word32+crcTable = V.generate 256 (go 8 . fromIntegral)+  where+    go :: Int -> Word32 -> Word32+    go 0 n = n+    go k n = go (k - 1) ((n `shiftR` 1) `xor` if testBit n 0 then 0xedb88320 else 0)+{-# NOINLINE crcTable #-}++getFromBinLogPacket :: Get a -> BinLogPacket -> IO a+getFromBinLogPacket g (BinLogPacket _ _ _ _ _ _ body _ ) =+    case parseDetailLazy (g <* endOfInput) body  of+        Left  (buf, offset, errmsg) -> throwIO (DecodePacketFailed buf offset errmsg)+        Right (_,   _,      r     ) -> return r++getFromBinLogPacket' :: (BinLogEventType -> Get a) -> BinLogPacket -> IO a+getFromBinLogPacket' g (BinLogPacket _ typ _ _ _ _ body _ ) =+    case parseDetailLazy (g typ <* endOfInput) body  of+        Left  (buf, offset, errmsg) -> throwIO (DecodePacketFailed buf offset errmsg)+        Right (_,   _,      r     ) -> return r++--------------------------------------------------------------------------------++data FormatDescription = FormatDescription+    { fdVersion              :: !Word16+    , fdMySQLVersion         :: !ByteString+    , fdCreateTime           :: !Word32+    -- , eventHeaderLen :: !Word8  -- const 19+    , fdEventHeaderLenVector :: !ByteString  -- ^ a array indexed by Binlog Event Type - 1+                                             -- to extract the length of the event specific header.+    } deriving (Show, Eq, Generic)++getFormatDescription :: Get FormatDescription+getFormatDescription = do+    version <- getWord16le+    server <- getByteString 50+    created <- getWord32le+    header <- getWord8+    unless (version == 4 && header == 19) (fail "unsupported binlog format/header length")+    rawLengths <- getRemainingLazyByteString+    when (L.null rawLengths || L.length rawLengths > 257) (fail "invalid format-description vector length")+    let lengths = L.toStrict rawLengths+    when (B.last lengths > 1) (fail "missing/unsupported checksum descriptor")+    pure (FormatDescription version (B.copy server) created (B.copy (B.init lengths)))++eventHeaderLen :: FormatDescription -> BinLogEventType -> Word8+eventHeaderLen fd typ+    | idx < 0 || idx >= B.length vec = 0+    | otherwise = B.unsafeIndex vec idx+  where+    vec :: ByteString+    vec = fdEventHeaderLenVector fd+    idx :: Int+    idx = fromEnum typ - 1++data RotateEvent = RotateEvent+    { rPos :: !Word64, rFileName :: !ByteString } deriving (Show, Eq)++getRotateEvent :: Get RotateEvent+getRotateEvent = do+    position <- getWord64le+    name <- getRemainingByteString+    unless (position >= 4 && not (B.null name) && B.length name <= 512 && not (B.elem 0 name))+        (fail "invalid rotate filename/position")+    pure (RotateEvent position name)++-- | This's query parser for statement based binlog's query event, it's actually+-- not used in row based binlog.+--+data QueryEvent = QueryEvent+    { qSlaveProxyId :: !Word32+    , qExecTime     :: !Word32+    , qErrCode      :: !Word16+    , qStatusVars   :: !ByteString+    , qSchemaName   :: !ByteString+    , qQuery        :: !Query+    } deriving (Show, Eq, Generic)++getQueryEvent :: Get QueryEvent+getQueryEvent = do+    pid <- getWord32le+    tim <- getWord32le+    slen <- getWord8+    ecode <- getWord16le+    vlen <- getWord16le+    svar <- getByteString (fromIntegral vlen)+    schema <- getByteString (fromIntegral slen)+    _ <- getWord8+    qry <- getRemainingLazyByteString+    return (QueryEvent pid tim ecode svar schema (Query qry))++-- | This's the query event in row based binlog.+--+data QueryEvent' = QueryEvent' { qQuery' :: !Query } deriving (Show, Eq)++getQueryEvent' :: Get QueryEvent'+getQueryEvent' = do+    _ <- getWord8+    QueryEvent' . Query <$> getRemainingLazyByteString++-- | Metadata validated by 'getTableMapEvent'. Low-level row parsers require+-- the corresponding parsed map; they reuse its proven metadata cardinality.+data TableMapEvent = TableMapEvent+    { tmTableId    :: !Word64+    , tmFlags      :: !Word16+    , tmSchemaName :: !ByteString+    , tmTableName  :: !ByteString+    , tmColumnCnt  :: !Int+    , tmColumnType :: ![FieldType]+    , tmColumnMeta :: ![BinLogMeta]+    , tmOptionalMetadata :: ![(Word8, ByteString)] -- ^ Owned, length-validated MariaDB TLVs, including unknown kinds.+    , tmNullMap    :: !ByteString+    } deriving (Show, Eq, Generic)++getTableMapEvent :: FormatDescription -> Get TableMapEvent+getTableMapEvent fd = do+    let hlen = eventHeaderLen fd BINLOG_TABLE_MAP_EVENT+    unless (hlen == 6 || hlen == 8) (fail "table map: unsupported post-header length")+    tid <- if hlen == 6 then fromIntegral <$> getWord32le else getWord48le+    flgs <- getWord16le+    slen <- getWord8+    schema <- getByteString (fromIntegral slen)+    schemaTerminator <- getWord8+    unless (schemaTerminator == 0) (fail "table map: missing name terminator")+    tlen <- getWord8+    table <- getByteString (fromIntegral tlen)+    zero <- getWord8+    unless (zero == 0) (fail "table map: missing name terminator")+    cc <- getLenEncInt+    when (cc <= 0 || cc > 4096) (fail "table map: invalid/excessive column count")+    colTypBS <- getByteString cc+    let typs = map FieldType (B.unpack colTypBS)+    metadataSize <- getLenEncInt+    when (metadataSize > 2 * cc) (fail "table map: excessive type metadata")+    colMetaBS <- getByteString metadataSize++    metas <- case runGetOrFail (forM typs getBinLogMeta) (L.fromStrict colMetaBS) of+        Left (_, _, errmsg) -> fail errmsg+        Right (rest, _, r) | L.null rest -> return r+        Right _ -> fail "table map: unused column metadata"++    nullmap <- getByteString ((cc + 7) `div` 8)+    optional <- getOptionalMetadata 256+    return (TableMapEvent tid flgs (B.copy schema) (B.copy table) cc typs metas optional (B.copy nullmap))++-- Unknown TLVs remain available to consumers (signedness, charset and column+-- names must not be invented from a SQL query against a potentially newer DDL).+getOptionalMetadata :: Int -> Get [(Word8, ByteString)]+getOptionalMetadata budget = do+    empty <- isEmpty+    if empty then pure [] else do+        when (budget <= 0) (fail "table map: optional metadata item budget exceeded")+        kind <- getWord8+        value <- getLenEncBytes+        ((kind,B.copy value):) <$> getOptionalMetadata (budget - 1)++data DeleteRowsEvent = DeleteRowsEvent+    { deleteTableId    :: !Word64+    , deleteFlags      :: !Word16+    -- , deleteExtraData   :: !RowsEventExtraData+    , deleteColumnCnt  :: !Int+    , deletePresentMap :: !BitMap+    , deleteRowData    :: ![[BinLogValue]]+    } deriving (Show, Eq, Generic)++-- | Decode delete images using the matching validated table map.+getDeleteRowEvent :: FormatDescription -> TableMapEvent -> BinLogEventType -> Get DeleteRowsEvent+getDeleteRowEvent fd tme typ = do+    let hlen = eventHeaderLen fd typ+        version2 = typ `elem` [BINLOG_WRITE_ROWS_EVENTv2,BINLOG_UPDATE_ROWS_EVENTv2,BINLOG_DELETE_ROWS_EVENTv2]+    unless (if version2 then hlen == 10 else hlen == 6 || hlen == 8)+        (fail "rows: unsupported post-header length")+    tid <- if hlen == 6 then fromIntegral <$> getWord32le else getWord48le+    flgs <- getWord16le+    when (typ == BINLOG_DELETE_ROWS_EVENTv2) $ do+        extraLen <- getWord16le+        when (extraLen < 2) (fail "rows: invalid extra-header length")+        skip (fromIntegral extraLen - 2)+    unless (tid == tmTableId tme) (fail "rows: table ID does not match metadata")+    colCnt <- getLenEncInt+    -- Metadata cardinality was proved when the map was parsed and cached.+    -- Compare the stored count in O(1); rescanning the cached list here makes+    -- sparse events pay for every omitted column again.+    unless (colCnt > 0 && colCnt <= 4096 && colCnt == tmColumnCnt tme) (fail "rows: column count does not match metadata")+    let (plen, poffset) = (colCnt + 7) `quotRem` 8+    pmap <- getPresentMap plen poffset+    DeleteRowsEvent tid flgs colCnt pmap <$>+        getNonemptyRows length (getBinLogRow (tmColumnMeta tme) pmap)++data WriteRowsEvent = WriteRowsEvent+    { writeTableId    :: !Word64+    , writeFlags      :: !Word16+    -- , writeExtraData   :: !RowsEventExtraData+    , writeColumnCnt  :: !Int+    , writePresentMap :: !BitMap+    , writeRowData    :: ![[BinLogValue]]+    } deriving (Show, Eq, Generic)++-- | Decode insert images using the matching validated table map.+getWriteRowEvent :: FormatDescription -> TableMapEvent -> BinLogEventType -> Get WriteRowsEvent+getWriteRowEvent fd tme typ = do+    let hlen = eventHeaderLen fd typ+        version2 = typ `elem` [BINLOG_WRITE_ROWS_EVENTv2,BINLOG_UPDATE_ROWS_EVENTv2,BINLOG_DELETE_ROWS_EVENTv2]+    unless (if version2 then hlen == 10 else hlen == 6 || hlen == 8)+        (fail "rows: unsupported post-header length")+    tid <- if hlen == 6 then fromIntegral <$> getWord32le else getWord48le+    flgs <- getWord16le+    when (typ == BINLOG_WRITE_ROWS_EVENTv2) $ do+        extraLen <- getWord16le+        when (extraLen < 2) (fail "rows: invalid extra-header length")+        skip (fromIntegral extraLen - 2)+    unless (tid == tmTableId tme) (fail "rows: table ID does not match metadata")+    colCnt <- getLenEncInt+    -- Metadata cardinality was proved when the map was parsed and cached.+    -- Compare the stored count in O(1); rescanning the cached list here makes+    -- sparse events pay for every omitted column again.+    unless (colCnt > 0 && colCnt <= 4096 && colCnt == tmColumnCnt tme) (fail "rows: column count does not match metadata")+    let (plen, poffset) = (colCnt + 7) `quotRem` 8+    pmap <- getPresentMap plen poffset+    WriteRowsEvent tid flgs colCnt pmap <$>+        getNonemptyRows length (getBinLogRow (tmColumnMeta tme) pmap)++data UpdateRowsEvent = UpdateRowsEvent+    { updateTableId    :: !Word64+    , updateFlags      :: !Word16+    -- , updateExtraData   :: !RowsEventExtraData+    , updateColumnCnt  :: !Int+    , updatePresentMap :: !(BitMap, BitMap)+    , updateRowData    :: ![ ([BinLogValue], [BinLogValue]) ]+    } deriving (Show, Eq, Generic)++-- | Decode both update images using the matching validated table map.+getUpdateRowEvent :: FormatDescription -> TableMapEvent -> BinLogEventType -> Get UpdateRowsEvent+getUpdateRowEvent fd tme typ = do+    let hlen = eventHeaderLen fd typ+        version2 = typ `elem` [BINLOG_WRITE_ROWS_EVENTv2,BINLOG_UPDATE_ROWS_EVENTv2,BINLOG_DELETE_ROWS_EVENTv2]+    unless (if version2 then hlen == 10 else hlen == 6 || hlen == 8)+        (fail "rows: unsupported post-header length")+    tid <- if hlen == 6 then fromIntegral <$> getWord32le else getWord48le+    flgs <- getWord16le+    when (typ == BINLOG_UPDATE_ROWS_EVENTv2) $ do+        extraLen <- getWord16le+        when (extraLen < 2) (fail "rows: invalid extra-header length")+        skip (fromIntegral extraLen - 2)+    unless (tid == tmTableId tme) (fail "rows: table ID does not match metadata")+    colCnt <- getLenEncInt+    -- Metadata cardinality was proved when the map was parsed and cached.+    -- Compare the stored count in O(1); rescanning the cached list here makes+    -- sparse events pay for every omitted column again.+    unless (colCnt > 0 && colCnt <= 4096 && colCnt == tmColumnCnt tme) (fail "rows: column count does not match metadata")+    let (plen, poffset) = (colCnt + 7) `quotRem` 8+    pmap <- getPresentMap plen poffset+    pmap' <- getPresentMap plen poffset+    UpdateRowsEvent tid flgs colCnt (pmap, pmap') <$>+        getNonemptyRows (\(before,after) -> length before + length after)+            ((,) <$> getBinLogRow (tmColumnMeta tme) pmap <*> getBinLogRow (tmColumnMeta tme) pmap')++rowBudget :: Int -> Int+rowBudget columns = min 65536 (1048576 `div` max 1 columns)++-- Each row event must contain at least one image; accepting an empty event+-- could expose a transaction checkpoint after silently losing its changes.+getNonemptyRows :: (a -> Int) -> Get a -> Get [a]+getNonemptyRows valueCount row = do+    empty <- isEmpty+    when empty (fail "rows: event contains no row images")+    first <- progressingRow row+    -- Every change uses the same event bitmaps. The first decoded image gives+    -- the exact value count, including NULLs, without another bitmap scan or+    -- any work for omitted columns. One change has at most 8192 values, below+    -- the global value budget; calculate the remaining allowance before a+    -- second change is decoded. Update before/after images both count.+    rest <- getRows (rowBudget (valueCount first) - 1) row+    pure (first : rest)++-- Empty images must still consume their null bitmap/values. A zero-progress+-- parser is rejected instead of looping forever on a corrupt present bitmap.+getRows :: Int -> Get a -> Get [a]+getRows budget row = do+    empty <- isEmpty+    if empty then pure [] else do+        when (budget <= 0) (fail "rows: decoded row/value budget exceeded")+        value <- progressingRow row+        (value :) <$> getRows (budget - 1) row++progressingRow :: Get a -> Get a+progressingRow row = do+    before <- bytesRead+    value <- row+    after <- bytesRead+    unless (after > before) (fail "rows: zero-length row image")+    pure value++getPresentMap :: Int -> Int -> Get BitMap+getPresentMap plen poffset = do+    pmap <- getByteString plen+    -- quotRem is applied to (columns + 7), so remainder 7 means an exact+    -- multiple of eight columns. Unused high bits must never count as columns.+    let bits = poffset + 1+        mask = if bits == 8 then 255 else (1 `shiftL` bits) - 1+    when (not (B.null pmap) && B.last pmap .&. complement mask /= 0)+        (fail "rows: nonzero padding in present bitmap")+    pure (BitMap pmap)
+ src/Database/MySQL/BinLogProtocol/BinLogMeta.hs view
@@ -0,0 +1,133 @@+{-# OPTIONS_GHC -funbox-strict-fields #-}++{-|+Module      : Database.MySQL.BinLogProtocol.BinLogMeta+Description : Binlog protocol column meta+Copyright   : (c) Winterland, 2016+License     : BSD+Maintainer  : Anton Antich <anton.antich@gmail.com>+Stability   : experimental+Portability : PORTABLE++This module provide column meta decoder for binlog protocol.++There're certain type won't appear in binlog event, and some types are compressed into 'mySQLTypeString'+, please take python version as a reference:  <https://github.com/noplay/python-mysql-replication>++You will not directly meet following t'FieldType' namely:++    * mySQLTypeDecimal+    * mySQLTypeNewdate+    * mySQLTypeEnum+    * mySQLTypeSet+    * mySQLTypeTinyBlob+    * mySQLTypeMediumBlOb+    * mySQLTypeLongBlob++-}++module Database.MySQL.BinLogProtocol.BinLogMeta (BinLogMeta(..), getBinLogMeta) where++import           Control.Monad (unless)+import           Data.Binary.Get+import           Data.Bits+import           Data.Word+import           Database.MySQL.Protocol.ColumnDef++-- | An intermedia date type for decoding row-based event's values.+--+data BinLogMeta+    = BINLOG_TYPE_TINY+    | BINLOG_TYPE_SHORT+    | BINLOG_TYPE_INT24+    | BINLOG_TYPE_LONG+    | BINLOG_TYPE_LONGLONG+    | BINLOG_TYPE_FLOAT       !Word8         -- ^ size+    | BINLOG_TYPE_DOUBLE      !Word8         -- ^ size+    | BINLOG_TYPE_BIT         !Word16 !Word8 -- ^ bits, bytes+    | BINLOG_TYPE_TIMESTAMP+    | BINLOG_TYPE_DATETIME+    | BINLOG_TYPE_DATE+    | BINLOG_TYPE_TIME+    | BINLOG_TYPE_TIMESTAMP2  !Word8         -- ^ fsp+    | BINLOG_TYPE_DATETIME2   !Word8         -- ^ fsp+    | BINLOG_TYPE_TIME2       !Word8         -- ^ fsp+    | BINLOG_TYPE_YEAR+    | BINLOG_TYPE_NEWDECIMAL  !Word8 !Word8  -- ^ precision, scale+    | BINLOG_TYPE_ENUM        !Word8         -- ^ 1 or 2('Word8' or 'Word16'), enum index size+    | BINLOG_TYPE_SET         !Word16 !Word8 -- ^ bitmap bits, bytes+    | BINLOG_TYPE_BLOB        !Word8         -- ^ length size+    | BINLOG_TYPE_STRING      !Word16        -- ^ meta length(if < 256, then length is 8bit,+                                             -- if > 256 then length is 16bit)+    | BINLOG_TYPE_GEOMETRY    !Word8         -- ^ length size+  deriving (Show, Eq)++getBinLogMeta :: FieldType -> Get BinLogMeta+getBinLogMeta t+    | t == mySQLTypeTiny       = pure BINLOG_TYPE_TINY+    | t == mySQLTypeShort      = pure BINLOG_TYPE_SHORT+    | t == mySQLTypeInt24      = pure BINLOG_TYPE_INT24+    | t == mySQLTypeLong       = pure BINLOG_TYPE_LONG+    | t == mySQLTypeLongLong   = pure BINLOG_TYPE_LONGLONG+    | t == mySQLTypeFloat      = BINLOG_TYPE_FLOAT <$> checkedByte "FLOAT width" (==4)+    | t == mySQLTypeDouble     = BINLOG_TYPE_DOUBLE <$> checkedByte "DOUBLE width" (==8)++    | t == mySQLTypeBit        = do+        byte0 <- getWord8+        byte1 <- getWord8+        let nbits = (fromIntegral byte1 `shiftL` 3) .|.  fromIntegral byte0+            nbytes = fromIntegral $ (nbits + 7) `shiftR` 3+        unless (byte0 <= 7 && nbits >= 1 && nbits <= 64) (fail "invalid BIT metadata")+        pure (BINLOG_TYPE_BIT nbits nbytes)++    | t == mySQLTypeTimestamp  = pure BINLOG_TYPE_TIMESTAMP+    | t == mySQLTypeDateTime   = pure BINLOG_TYPE_DATETIME+    | t == mySQLTypeDate       = pure BINLOG_TYPE_DATE+    | t == mySQLTypeTime       = pure BINLOG_TYPE_TIME+    | t == mySQLTypeTimestamp2 = BINLOG_TYPE_TIMESTAMP2 <$> checkedByte "fractional precision" (<=6)+    | t == mySQLTypeDateTime2  = BINLOG_TYPE_DATETIME2 <$> checkedByte "fractional precision" (<=6)+    | t == mySQLTypeTime2      = BINLOG_TYPE_TIME2 <$> checkedByte "fractional precision" (<=6)+    | t == mySQLTypeYear       = pure BINLOG_TYPE_YEAR+    | t == mySQLTypeNewDecimal = do+        precision <- getWord8+        scale <- getWord8+        unless (precision >= 1 && precision <= 65 && scale <= 38 && scale <= precision)+            (fail "invalid DECIMAL metadata")+        pure (BINLOG_TYPE_NEWDECIMAL precision scale)+    | t == mySQLTypeVarChar    = BINLOG_TYPE_STRING <$> getWord16le+    | t == mySQLTypeVarString  = BINLOG_TYPE_STRING <$> getWord16le++    | t == mySQLTypeString     = do+        byte0 <- getWord8+        byte1 <- getWord8+        -- http://bugs.mysql.com/37426+        if  byte0 > 0+        then if (byte0 .&. 0x30) /= 0x30+             then if FieldType (byte0 .|. 0x30) == mySQLTypeString+                  then let len = fromIntegral $ (byte0 .&. 0x30) `xor` 0x30+                           len' = len `shiftL` 4 .|. fromIntegral byte1+                       in pure $! BINLOG_TYPE_STRING len'+                  else let len = fromIntegral byte0 `shiftL` 8 :: Word16+                           len' = len .|. fromIntegral byte1+                       in pure $! BINLOG_TYPE_STRING len'+             else let t' = FieldType byte0+                  in if | t' == mySQLTypeSet    -> let nbits = fromIntegral byte1 `shiftL` 3+                                                       nbytes = fromIntegral $ (nbits + 7) `shiftR` 3+                                                   in if byte1 >= 1 && byte1 <= 8 then pure (BINLOG_TYPE_SET nbits nbytes) else fail "invalid SET width"+                        | t' == mySQLTypeEnum   -> if byte1 == 1 || byte1 == 2 then pure (BINLOG_TYPE_ENUM byte1) else fail "invalid ENUM width"+                        | t' == mySQLTypeString -> pure (BINLOG_TYPE_STRING (fromIntegral byte1))+                        | otherwise             -> fail $ "Database.MySQL.BinLogProtocol.BinLogMeta:\+                                                           \ impossible type inside binlog string: " ++ show t'+        else pure (BINLOG_TYPE_STRING (fromIntegral byte1))++    | t == mySQLTypeBlob       = BINLOG_TYPE_BLOB <$> checkedByte "BLOB length width" (\n -> n >= 1 && n <= 4)+    | t == mySQLTypeGeometry   = BINLOG_TYPE_GEOMETRY <$> checkedByte "GEOMETRY length width" (\n -> n >= 1 && n <= 4)+    | otherwise                = fail $ "Database.MySQL.BinLogProtocol.BinLogMeta:\+                                        \ impossible type in binlog: " ++ show t++-- Validate metadata before it can drive byte counts or temporal arithmetic.+checkedByte :: String -> (Word8 -> Bool) -> Get Word8+checkedByte label valid = do+    value <- getWord8+    unless (valid value) (fail ("invalid binlog " ++ label))+    pure value
+ src/Database/MySQL/BinLogProtocol/BinLogValue.hs view
@@ -0,0 +1,338 @@+{-|+Module      : Database.MySQL.BinLogProtocol.BinLogValue+Description : Binlog protocol+Copyright   : (c) Winterland, 2016+License     : BSD+Maintainer  : Anton Antich <anton.antich@gmail.com>+Stability   : experimental+Portability : PORTABLE++Binlog protocol++-}++module Database.MySQL.BinLogProtocol.BinLogValue+    ( BinLogValue(..), getBinLogField, getMicroSecond, getBits', getBinLogRow+    ) where++import           Control.Monad                            (unless)+import           Data.Binary.Get+import           Data.Binary.Put                          ()+import           Data.Bits+import           Data.ByteString                          (ByteString)+import qualified Data.ByteString                          as B+import qualified Data.ByteString.Unsafe                   as B+import           Data.Int+import           Data.Int.Int24+import           Data.Scientific+import           Data.Word+import           Database.MySQL.BinLogProtocol.BinLogMeta+import           Database.MySQL.Protocol.MySQLValue+import           Database.MySQL.Protocol.Packet+import           GHC.Generics                             (Generic)++-- | Data type for representing binlog values.+--+-- Values preserve storage-level numeric and temporal representations. Text and+-- binary columns both use bytes: decoding a character set requires the relevant+-- table metadata, which can differ from the connection's current character set.+--+-- Base column types do not encode signedness. Optional table-map metadata can+-- supply it, but this value decoder leaves integral bits in their inherited+-- signed-width constructors. For an unsigned TINYINT, for example,+-- @fromIntegral (-1 :: Int8) :: Word8@ recovers 255; use the matching width.+-- INT24 similarly converts through the package's Word24 type.+--+-- Timestamps represent server UTC fields; zero retains MariaDB's zero timestamp+-- rather than becoming the Unix epoch. ENUM is its stored index and SET its+-- stored bitmap. Their labels and text collations belong to table metadata.+--+data BinLogValue+    = BinLogTiny       !Int8+    | BinLogShort      !Int16+    | BinLogInt24      !Int24+    | BinLogLong       !Int32+    | BinLogLongLong   !Int64+    | BinLogFloat      !Float+    | BinLogDouble     !Double+    | BinLogBit        !Word64          -- ^ a 64bit bitmap.+    | BinLogTimeStamp  !Word32          -- ^ a utc timestamp, note 0 doesn't mean @1970-01-01 00:00:00@,+                                        -- because mysql choose 0 to present '0000-00-00 00:00:00'+    | BinLogTimeStamp2 !Word32 !Word32  -- ^ like 'BinLogTimeStamp' with an addtional microseconds field.+    | BinLogDateTime   !Word16 !Word8 !Word8 !Word8 !Word8 !Word8         -- ^ YYYY MM DD hh mm ss+    | BinLogDateTime2  !Word16 !Word8 !Word8 !Word8 !Word8 !Word8 !Word32 -- ^ YYYY MM DD hh mm ss microsecond+    | BinLogDate       !Word16 !Word8 !Word8                   -- ^ YYYY MM DD+    | BinLogTime       !Word8  !Word16 !Word8 !Word8           -- ^ sign(1= non-negative, 0= negative) hh mm ss+    | BinLogTime2      !Word8  !Word16 !Word8 !Word8 !Word32   -- ^ sign(1= non-negative, 0= negative) hh mm ss microsecond+    | BinLogYear       !Word16                                 -- ^ year value, 0 stand for '0000'+    | BinLogNewDecimal !Scientific                             -- ^ sign(1= non-negative, 0= negative) integeral part, fractional part+    | BinLogEnum       !Word16                                 -- ^ enum indexing value+    | BinLogSet        !Word64                                 -- ^ set indexing 64bit bitmap.+    | BinLogBytes      !ByteString                             -- ^ all string and blob values.+    | BinLogGeometry   !ByteString+    | BinLogNull+  deriving (Show, Eq, Generic)++--------------------------------------------------------------------------------+-- | BinLog protocol decoder+--+getBinLogField :: BinLogMeta -> Get BinLogValue+getBinLogField BINLOG_TYPE_TINY                = BinLogTiny     <$> getInt8+getBinLogField BINLOG_TYPE_SHORT               = BinLogShort    <$> getInt16le+getBinLogField BINLOG_TYPE_INT24               = BinLogInt24 . fromIntegral <$> getWord24le+getBinLogField BINLOG_TYPE_LONG                = BinLogLong     <$> getInt32le+getBinLogField BINLOG_TYPE_LONGLONG            = BinLogLongLong <$> getInt64le+getBinLogField (BINLOG_TYPE_FLOAT  _         ) = BinLogFloat <$> getFloatle+getBinLogField (BINLOG_TYPE_DOUBLE _         ) = BinLogDouble <$> getDoublele+getBinLogField (BINLOG_TYPE_BIT    _    bytes) = BinLogBit <$> getBits' bytes+getBinLogField BINLOG_TYPE_TIMESTAMP           = BinLogTimeStamp <$> getWord32le++-- A integer in @YYYYMMDD@ format, for example:+-- 99991231 stand for @9999-12-31@+getBinLogField BINLOG_TYPE_DATE = do+    i <- getWord24le+    let (i', dd) = i `quotRem` 32+        (yyyy, mm) = i' `quotRem` 16+    pure (BinLogDate (fromIntegral yyyy)+                     (fromIntegral mm)+                     (fromIntegral dd))++getBinLogField (BINLOG_TYPE_TIMESTAMP2  fsp) = do+    s <- getWord32be -- big-endian here!+    ms <- getMicroSecond fsp >>= checkedFraction fsp+    pure (BinLogTimeStamp2 s ms)++-- A integer in @YYYYMMDDhhmmss@, for example:+-- 99991231235959 stand for @9999-12-31 23:59:59@+getBinLogField BINLOG_TYPE_DATETIME = do+    i <- getWord64le+    let (yyyy, i')   = i      `quotRem` 10000000000+        (mm, i'')    = i'     `quotRem` 100000000+        (dd, i''')   = i''    `quotRem` 1000000+        (h, i'''')   = i'''   `quotRem` 10000+        (m, s)       = i''''  `quotRem` 100+    pure (BinLogDateTime (fromIntegral yyyy)+                         (fromIntegral mm)+                         (fromIntegral dd)+                         (fromIntegral h)+                         (fromIntegral m)+                         (fromIntegral s))++-- BINLOG_TYPE_DATETIME2(big endian)+--+-- 1 bit sign (used when on disk)+-- 17 bits year * 13 + month (year 0-9999, month 0-12)+-- 5 bits day (0-31)+-- 5 bits hour (0-23)+-- 6 bits minute (0-59)+-- 6 bits second (0-59)+-- (5 bytes in total)+--+-- fractional-seconds storage (size depends on meta)+--+getBinLogField (BINLOG_TYPE_DATETIME2 fsp) = do+    iPart <- getWord40be+    let yyyymm = iPart `shiftR` 22 .&. 0x01FFFF -- 0b011111111111111111+        (yyyy, mm) = yyyymm `quotRem` 13+        yyyy' = fromIntegral yyyy+        mm' = fromIntegral mm+        dd = fromIntegral $ iPart `shiftR` 17 .&. 0x1F -- 0b00011111+        h =  fromIntegral $ iPart `shiftR` 12 .&. 0x1F -- 0b00011111+        m =  fromIntegral $ iPart `shiftR` 6 .&. 0x3F  -- 0b00111111+        s =  fromIntegral $ iPart .&. 0x3F             -- 0b00111111+    ms <- getMicroSecond fsp >>= checkedFraction fsp+    pure (BinLogDateTime2 yyyy' mm' dd h m s ms)++-- A integer in @hhmmss@ format(can be negative), for example:+-- 8385959 stand for @838:59:59@+getBinLogField BINLOG_TYPE_TIME = do+    i <- getWord24le+    let i' =  fromIntegral i :: Int24+        sign = if i' >= 0 then 1 else 0+    let (h, i'')     = i'     `quotRem` 10000+        (m, s)       = i''    `quotRem` 100+    pure (BinLogTime sign (fromIntegral (abs h))+                          (fromIntegral (abs m))+                          (fromIntegral (abs s)))++-- BINLOG_TYPE_TIME2(big endian)+--+-- 1 bit sign  (1= non-negative, 0= negative)+-- 1 bit unused (Reserved for wider hour range, e.g. for intervals)+-- 10 bit hour (0-838)+-- 6 bit minute (0-59)+-- 6 bit second (0-59)+-- (3 bytes in total)+--+-- fractional-seconds storage (size depends on meta)+--+getBinLogField (BINLOG_TYPE_TIME2 fsp) = do+    raw <- getWord24be+    fraction <- getMicroSecond fsp+    let integerPart = fromIntegral raw - 0x800000 :: Int64+        negative = integerPart < 0+        -- Binary ordering borrows one integer unit only when a negative time+        -- has a nonzero fractional part (MariaDB sql/compat56.cc). Whole+        -- negative seconds must not lose another second during decoding.+        magnitude = abs (if negative && fraction /= 0 then integerPart + 1 else integerPart)+        hours = fromIntegral (magnitude `shiftR` 12) .&. 0x03ff+        minutes = fromIntegral (magnitude `shiftR` 6) .&. 0x3f+        seconds = fromIntegral magnitude .&. 0x3f+    unless (if negative then fraction <= 0 else fraction >= 0)+        (fail "TIME2: inconsistent fractional sign")+    micros <- checkedFraction fsp (abs fraction)+    pure (BinLogTime2 (if negative then 0 else 1) hours minutes seconds micros)++getBinLogField BINLOG_TYPE_YEAR                = do+    y <- getWord8+    pure $! if y == 0 then BinLogYear 0 else BinLogYear (1900 + fromIntegral y)++-- Decimal representation in binlog seems to be as follows:+--+-- 1st bit - sign such that set == +, unset == -+-- every 4 bytes represent 9 digits in big-endian order.+--+-- 80 00 00 05 1b 38 b0 60 00 means:+--+--   0x80 - positive+--   0x00000005 - 5+--   0x1b38b060 - 456700000+--   0x00       - 0+--+-- 54567000000 / 10^{10} = 5.4567+--+-- if there're < 9 digits at first, it will be compressed into suitable length words+-- following a simple lookup table.+--+getBinLogField (BINLOG_TYPE_NEWDECIMAL precision scale)+    | precision == 0 || precision > 65 || scale > 38 || precision < scale = fail "NEWDECIMAL: invalid precision/scale"+    | otherwise = do+    let i = (precision - scale)+        (ucI, cI) = i `quotRem` digitsPerInteger+        (ucF, cF) = scale `quotRem` digitsPerInteger+        ucISize = fromIntegral (ucI `shiftL` 2)+        ucFSize = fromIntegral (ucF `shiftL` 2)+        cISize = fromIntegral (sizeTable `B.unsafeIndex` fromIntegral cI)+        cFSize = fromIntegral (sizeTable `B.unsafeIndex` fromIntegral cF)+        len = ucISize + cISize + ucFSize + cFSize++    buf <- getByteString len++    let fb = buf `B.unsafeIndex` 0+        sign = if fb .&. 0x80 == 0x80 then 1 else 0 :: Word8+        buf' = (fb `xor` 0x80) `B.cons` B.tail buf+        buf'' = if sign == 1 then buf'+                            else B.map (xor 0xFF) buf'++        iPart = fromIntegral (getCompressed cISize (B.unsafeTake cISize buf'')) * (blockSize ^ ucI)+              + getUncompressed ucI (B.unsafeDrop cISize buf'')++    let buf''' = B.unsafeDrop (ucISize + cISize) buf''++        fPart = getUncompressed ucF (B.unsafeTake ucFSize buf''') * (10 ^ cF)+              + fromIntegral (getCompressed cFSize (B.unsafeDrop ucFSize buf'''))++    let sci = scientific (iPart * 10 ^ scale + fPart) (negate $ fromIntegral scale)+        sci' = if sign == 0 then negate sci else sci+    pure (BinLogNewDecimal sci')+  where+    digitsPerInteger = 9+    blockSize = fromIntegral $ (10 :: Int32) ^ (9 :: Int)+    sizeTable = B.pack [0, 1, 1, 2, 2, 3, 3, 4, 4, 4]++    getCompressed :: Int -> ByteString -> Word64+    getCompressed 0 _  = 0+    getCompressed x bs = let fb = bs `B.unsafeIndex` 0+                             x' = x - 1+                         in fromIntegral fb `shiftL` (8 * x') .|. getCompressed x' (B.unsafeDrop 1 bs)++    getUncompressed :: Word8 -> ByteString -> Integer+    getUncompressed 0 _ = 0+    getUncompressed x bs = let v = getCompressed 4 (B.unsafeTake 4 bs)+                               x' = x - 1+                           in fromIntegral v * (blockSize ^ x') + getUncompressed x' (B.unsafeDrop 4 bs)+++getBinLogField (BINLOG_TYPE_ENUM size) =+    if  | size == 1 -> BinLogEnum . fromIntegral <$> getWord8+        | size == 2 -> BinLogEnum <$> getWord16le+        | otherwise -> fail $ "Database.MySQL.BinLogProtocol.BinLogValue: wrong \+                              \BINLOG_TYPE_ENUM size: " ++ show size+++getBinLogField (BINLOG_TYPE_SET _ bytes)+    | bytes == 0 || bytes > 8 = fail "invalid SET storage width"+    | otherwise = BinLogSet . B.foldr (\byte rest -> fromIntegral byte .|. (rest `shiftL` 8)) 0 <$> getByteString (fromIntegral bytes)+getBinLogField (BINLOG_TYPE_BLOB lensize) = do+    len <- if  | lensize == 1 -> fromIntegral <$> getWord8+               | lensize == 2 -> fromIntegral <$> getWord16le+               | lensize == 3 -> fromIntegral <$> getWord24le+               | lensize == 4 -> fromIntegral <$> getWord32le+               | otherwise    -> fail $ "Database.MySQL.BinLogProtocol.BinLogValue: \+                                        \wrong BINLOG_TYPE_BLOB length size: " ++ show lensize+    BinLogBytes <$> getByteString len++getBinLogField (BINLOG_TYPE_STRING size) = do+    len <- if | size < 256 -> fromIntegral <$> getWord8+              | otherwise  -> fromIntegral <$> getWord16le+    BinLogBytes <$> getByteString len++getBinLogField (BINLOG_TYPE_GEOMETRY lensize) = do+    len <- if | lensize == 1 -> fromIntegral <$> getWord8+              | lensize == 2 -> fromIntegral <$> getWord16le+              | lensize == 3 -> fromIntegral <$> getWord24le+              | lensize == 4 -> fromIntegral <$> getWord32le+              | otherwise    -> fail $  "Database.MySQL.BinLogProtocol.BinLogValue: \+                                        \wrong BINLOG_TYPE_GEOMETRY length size: " ++ show lensize+    BinLogGeometry <$> getByteString len++-- | Decode the signed fractional storage used by TIME2. Adjacent odd/even+-- precisions share a byte scale; the odd precision's last decimal digit is zero.+-- TIMESTAMP2 and DATETIME2 additionally require a nonnegative fraction.+getMicroSecond :: Word8 -> Get Int32+getMicroSecond 0 = pure 0+getMicroSecond 1 = (* 10000) . fromIntegral <$> getInt8+getMicroSecond 2 = (* 10000) . fromIntegral <$> getInt8+getMicroSecond 3 = (* 100) . fromIntegral <$> getInt16be+getMicroSecond 4 = (* 100) . fromIntegral <$> getInt16be+getMicroSecond 5 = fromIntegral <$> getInt24be+getMicroSecond 6 = fromIntegral <$> getInt24be+getMicroSecond _ = fail "invalid fractional-second precision"++-- Validate before converting to Word32: a negative/corrupt fraction must not+-- wrap to a large microsecond value or imply more precision than the column.+checkedFraction :: Word8 -> Int32 -> Get Word32+checkedFraction precision micros = do+    unless (precision <= 6 && micros >= 0 && micros < 1000000+            && micros `mod` (10 ^ (6 - precision)) == 0)+        (fail "invalid binlog fractional seconds")+    pure (fromIntegral micros)++getBits' :: Word8 -> Get Word64+getBits' bytes = if bytes <= 8+    then getBits (fromIntegral bytes)+    else fail $  "Database.MySQL.BinLogProtocol.BinLogValue: \+                 \wrong bit length size: " ++ show bytes++--------------------------------------------------------------------------------+-- | BinLog row decoder+--+getBinLogRow :: [BinLogMeta] -> BitMap -> Get [BinLogValue]+getBinLogRow metas pmap =+    -- Construct this Get once per event/image and reuse it for every row. The+    -- selected metadata list is shared, so sparse images do not rescan every+    -- omitted column for every row in a large event.+    let selected = [meta | (index,meta) <- zip [0..] metas, isColumnSet pmap index]+        maplen = (length selected + 7) `shiftR` 3+    in do+        nullmap <- getByteString maplen+        -- MariaDB initializes this bitmap to 0xff and clears non-NULL bits;+        -- unused high bits are allowed to remain set (sql/rpl_record.cc).+        go selected (BitMap nullmap) 0+  where+    go :: [BinLogMeta] -> BitMap -> Int -> Get [BinLogValue]+    go [] _ _ = pure []+    go (meta:rest) nullmap index = do+        value <- if isColumnSet nullmap index then pure BinLogNull else getBinLogField meta+        values <- go rest nullmap (index + 1)+        pure (value : values)
+ src/Database/MySQL/BinLogProtocol/CDC.hs view
@@ -0,0 +1,530 @@+{-# LANGUAGE BangPatterns #-}+-- | Stateful MariaDB event decoding. The decoder retains table metadata, never+-- transactions or row history; consumers control backpressure by reading once.+-- Application code normally uses the managed stream reexported by+-- "Database.MySQL.BinLog", which adds connection ownership, terminal failure+-- handling, GTID setup and acknowledgement validation. The pure/wire decoder+-- alone does not establish downstream durability.+module Database.MySQL.BinLogProtocol.CDC+    ( CDCError(..), Gtid(..), GtidSet, gtidSet, parseGtidSet, renderGtidSet, gtidSetEntries+    , GtidList(..)+    , BinLogPosition(..), DecodeLimits(..), defaultDecodeLimits+    , CDCCheckpoint, checkpointPosition, checkpointGtids+    , CDCEvent(..), CDCPayload(..), newCDCDecoder, validCheckpoint+    ) where++import Control.Exception (throwIO, catch, SomeException, Exception)+import Control.Monad+import qualified Codec.Compression.Zlib.Internal as Z+import Data.Binary.Get+import Data.Bits+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as C+import qualified Data.ByteString.Lazy as L+import Data.IORef+import qualified Data.Map.Strict as M+import Data.Unique+import Data.Word+import Database.MySQL.BinLogProtocol.BinLogEvent+import Database.MySQL.Protocol.Packet (getLenEncInt)+import Database.MySQL.Query (fromQuery)+import qualified System.IO.Streams as S+import Text.Read (readMaybe)++-- | CDC-specific failures, separate from packet decoding and connection+-- ownership/transport exceptions. Configuration/checkpoint misuse is rejected+-- locally; decoding, expansion and unsupported-stream failures are terminal+-- when read through 'Database.MySQL.BinLog.openCDC'.+data CDCError+    = CDCInvalidConfiguration !String+    | CDCInvalidCheckpoint !String+    | CDCDecodeError !String+    | CDCResourceLimit !String+    | CDCUnsupportedFeature !String+    deriving (Eq, Show)+instance Exception CDCError++-- | MariaDB identity, domain-server-sequence (not a UUID interval).+data Gtid = Gtid+    { gtidDomainId :: !Word32 -- ^ Independent MariaDB replication domain.+    , gtidServerId :: !Word32 -- ^ Originating server, not the consumer replica ID.+    , gtidSequence :: !Word64 -- ^ Positive sequence number in the domain history.+    } deriving (Eq, Show)++-- | One last completed transaction per replication domain. Construction rejects+-- duplicate domains: silently picking one can skip a different primary's work.+newtype GtidSet = GtidSet (M.Map Word32 Gtid) deriving (Eq, Show)++-- | Validate at most 4096 unique domains with positive sequence numbers.+gtidSet :: [Gtid] -> Either String GtidSet+gtidSet entries+    | length entries > 4096 = Left "GTID set exceeds 4096 domains"+    | any ((== 0) . gtidSequence) entries = Left "GTID sequence must be positive"+    | M.size result /= length entries = Left "duplicate GTID domain"+    | otherwise = Right (GtidSet result)+  where result = M.fromList [(gtidDomainId g, g) | g <- entries]++-- | Parse a bounded MariaDB connect-state string. Empty means start at the+-- beginning of available history; negative, overflowing or ambiguous IDs fail.+parseGtidSet :: B.ByteString -> Either String GtidSet+parseGtidSet bytes+    | B.length bytes > 65535 = Left "GTID set exceeds 65535 bytes"+    | B.null bytes = gtidSet []+    | otherwise = traverse parseOne (C.split ',' bytes) >>= gtidSet+  where+    number :: B.ByteString -> Maybe Integer+    number part | B.length part > 20 || B.null part || B.any (\c -> c < 48 || c > 57) part = Nothing+                | otherwise = readMaybe (C.unpack part)+    parseOne part = case C.split '-' part of+        [d,s,n] -> case (number d, number s, number n) of+            (Just domain,Just server,Just sequenceNo)+                | domain <= 0xffffffff && server <= 0xffffffff && sequenceNo <= 0xffffffffffffffff ->+                    Right (Gtid (fromInteger domain) (fromInteger server) (fromInteger sequenceNo))+            _ -> Left "invalid/overflowing MariaDB GTID"+        _ -> Left "expected domain-server-sequence"++-- | Canonical ascending-domain representation suitable for @slave_connect_state@.+renderGtidSet :: GtidSet -> B.ByteString+renderGtidSet = C.intercalate "," . map render . gtidSetEntries+  where render (Gtid d s n) = C.pack (show d ++ "-" ++ show s ++ "-" ++ show n)++-- | Inspect completed domain positions in canonical order.+gtidSetEntries :: GtidSet -> [Gtid]+gtidSetEntries (GtidSet entries) = M.elems entries++-- | Binlog history advertised by a GTID_LIST event. A domain can contain+-- several originating servers after failover; collapsing it into t'GtidSet'+-- would discard history or guess a resume position. This informational event+-- does not advance a transaction checkpoint.+data GtidList = GtidList+    { gtidListFlags :: !Word32 -- ^ Original upper four bits of the count word.+    , gtidListEntries :: ![Gtid] -- ^ All domain/server entries, in wire order.+    } deriving (Eq, Show)++-- | Binlog coordinates retain 64 bits. Traditional COM_BINLOG_DUMP accepts only+-- 32 bits; the connection API checks that boundary rather than truncating it.+data BinLogPosition = BinLogPosition+    { binlogFile :: !B.ByteString -- ^ Exact source binlog filename bytes.+    , binlogNextPosition :: !Word64 -- ^ Next event offset after the represented boundary.+    } deriving (Eq, Show)++-- | Independent retained-metadata and event expansion budgets. Exceeding a+-- budget fails the stream; eviction would make later row images ambiguous.+data DecodeLimits = DecodeLimits+    { maxTableMaps :: !Int -- ^ Concurrent table-map identities retained by the decoder.+    , maxTableMapBytes :: !Int -- ^ Aggregate owned table metadata; no silent eviction.+    , maxExpandedEventBytes :: !Int -- ^ Expanded/reassembled bytes for one event.+    } deriving (Eq, Show)++-- | 4096 maps, 16 MiB retained metadata and 64 MiB expanded/reassembled events.+defaultDecodeLimits :: DecodeLimits+defaultDecodeLimits = DecodeLimits 4096 (16 * 1024 * 1024) (64 * 1024 * 1024)++-- | Opaque complete-group token. Persist its exported position/state, never+-- its stream-local identity; only a live token can acknowledge that stream.+--+-- The unique decoder identity and delivery sequence cannot be manufactured by a+-- caller. Only completed transaction boundaries produce a checkpoint.+data CDCCheckpoint = CDCCheckpoint !Unique !Word64 !BinLogPosition !(Maybe GtidSet)+instance Show CDCCheckpoint where+    show token = "CDCCheckpoint " ++ show (checkpointPosition token) ++ " " ++ show (checkpointGtids token)+instance Eq CDCCheckpoint where+    CDCCheckpoint u n p g == CDCCheckpoint v m q h = u == v && n == m && p == q && g == h++-- | Next event coordinate after a complete delivered transaction/group.+checkpointPosition :: CDCCheckpoint -> BinLogPosition+checkpointPosition (CDCCheckpoint _ _ position _) = position++-- | Complete GTID resume state when the stream started from GTIDs. A file+-- position start has no proven initial GTID state, so returns Nothing; resume+-- it using the file coordinate. Persist the checkpoint atomically with+-- destination writes before acknowledging. This value alone proves no durability.+checkpointGtids :: CDCCheckpoint -> Maybe GtidSet+checkpointGtids (CDCCheckpoint _ _ _ gtids) = gtids++-- | Decoded event plus an optional complete-group checkpoint. Row images retain+-- present-column bitmaps; a missing column is distinct from @BinLogNull@.+data CDCEvent = CDCEvent+    { cdcEventPosition :: !BinLogPosition -- ^ Source next-event coordinate.+    , cdcEventTimestamp :: !Word32 -- ^ Source header timestamp in Unix seconds.+    , cdcEventPayload :: !CDCPayload -- ^ Decoded row/control event; sparse columns remain explicit.+    , cdcEventCheckpoint :: !(Maybe CDCCheckpoint)+      -- ^ Present only at a complete-group boundary, before destination persistence.+    , cdcEventAckRequested :: !Bool+      -- ^ Source requested semisync receipt; this alone never sends an ACK.+    } deriving (Eq, Show)++-- | Events needed to reproduce the change stream. Unsupported non-ignorable+-- events fail instead of silently crossing an unsafe checkpoint boundary.+data CDCPayload+    = CDCBegin !Gtid !Word8+    | CDCQuery !QueryEvent+    | CDCAnnotate !B.ByteString+    | CDCRowQuery !QueryEvent'+    | CDCWrite !TableMapEvent !WriteRowsEvent+    | CDCUpdate !TableMapEvent !UpdateRowsEvent+    | CDCDelete !TableMapEvent !DeleteRowsEvent+    | CDCCommit !(Maybe Word64)+    | CDCRollback+    | CDCRotate !RotateEvent+    | CDCFormat !FormatDescription+    | CDCGtidList !GtidList+    | CDCBinlogCheckpoint !B.ByteString+    | CDCHeartbeat+    | CDCStop+    | CDCIgnorable !BinLogPacket+    deriving (Eq, Show)++data Decoder = Decoder+    { format :: !FormatDescription, filename :: !B.ByteString+    , maps :: !(M.Map Word64 (Int,TableMapEvent)), mapBytes :: !Int+    , active :: !(Maybe (Gtid,Word8)), transactionOpen :: !Bool+    , completed :: !GtidSet, delivery :: !Word64+    }++-- | Check that a token belongs to this decoder and is no newer than the+-- available boundary. Connection code separately enforces monotone ACKs.+validCheckpoint :: CDCCheckpoint -> CDCCheckpoint -> Bool+validCheckpoint (CDCCheckpoint owner current _ _) (CDCCheckpoint candidate sequenceNo _ _) =+    owner == candidate && sequenceNo <= current++-- | Construct a single-consumer decoder and latest-boundary observer. No read+-- ahead or background worker is used. Live callers must wrap reads with their connection+-- ownership/error guard; standalone decoders propagate failures to the caller.+newCDCDecoder :: DecodeLimits -> Maybe GtidSet -> FormatDescription -> B.ByteString+              -> S.InputStream BinLogPacket+              -> IO (S.InputStream CDCEvent, IO (Maybe CDCCheckpoint))+newCDCDecoder limits initialGtids initialFormat initialFile input = do+    when (maxTableMaps limits <= 0 || maxTableMapBytes limits <= 0 || maxExpandedEventBytes limits <= 0)+        (throwIO (CDCInvalidConfiguration "positive CDC budgets required"))+    when (B.null initialFile || B.length initialFile > 512 || B.elem 0 initialFile+          || fdVersion initialFormat /= 4 || B.length (fdMySQLVersion initialFormat) > 50+          || B.length (fdEventHeaderLenVector initialFormat) > 256)+        (throwIO (CDCInvalidConfiguration "invalid initial filename/format"))+    inputEvents <- assembleEvents limits input+    owner <- newUnique+    let initialState = maybe (GtidSet M.empty) id initialGtids+        ownedFormat = initialFormat+            { fdMySQLVersion = B.copy (fdMySQLVersion initialFormat)+            , fdEventHeaderLenVector = B.copy (fdEventHeaderLenVector initialFormat)+            }+    state <- newIORef (Decoder ownedFormat (B.copy initialFile) M.empty 0 Nothing False initialState 0)+    latest <- newIORef Nothing+    ended <- newIORef False+    let step = do+            done <- readIORef ended+            if done then pure Nothing else do+                event <- S.read inputEvents+                current <- readIORef state+                case event of+                    Nothing -> do+                        writeIORef ended True+                        when (transactionOpen current || active current /= Nothing)+                            (bad "binlog ended inside an event group")+                        pure Nothing+                    Just packet -> do+                        expanded <- expandEvent limits (format current) packet+                        decode owner latest state current expanded+        decode ownerId latestRef stateRef current packet = do+            let typ = blEventType packet+                position = BinLogPosition (filename current) (blLogPos packet)+                emit next payload boundary = do+                    when (boundary && (blLogPos packet < 4 || B.null (filename current)))+                        (bad "transaction boundary has no resumable file position")+                    let sequenceNo = delivery current + 1+                        token = if boundary then Just (CDCCheckpoint ownerId sequenceNo position (completed next <$ initialGtids)) else Nothing+                    writeIORef stateRef next { delivery = sequenceNo }+                    forM_ token (writeIORef latestRef . Just)+                    pure (Just (CDCEvent position (blTimestamp packet) payload token (blSemiAck packet)))+                clear next = next { maps = M.empty, mapBytes = 0 }+                finish = do+                    newGtids <- case active current of+                        Nothing -> pure (completed current)+                        Just (g,_) -> advance g (completed current)+                    pure (clear current { active = Nothing, transactionOpen = False, completed = newGtids })+                row parser payload flags = do+                    tid <- getFromBinLogPacket (getTableId (format current) typ) packet+                    table <- maybe (bad "rows reference an unknown/stale table ID") (pure . snd) (M.lookup tid (maps current))+                    value <- getFromBinLogPacket' (parser (format current) table) packet+                    let endStatement = flags value .&. 1 /= 0+                        standalone = maybe False (\(_,f) -> f .&. 1 /= 0) (active current)+                    next <- if endStatement && standalone then finish+                            else pure (if endStatement then clear current else current)+                    emit next (payload table value) (endStatement && standalone)+            case typ of+                BINLOG_TABLE_MAP_EVENT -> do+                    when (toInteger (L.length (blBody packet)) > toInteger (maxTableMapBytes limits))+                        (throwIO (CDCResourceLimit "CDC table metadata wire budget exceeded"))+                    table <- getFromBinLogPacket (getTableMapEvent (format current)) packet+                    let key = tmTableId table+                        -- Account the complete wire metadata plus conservative+                        -- list/constructor overhead; copy only retained slices.+                        charge = toInteger (L.length (blBody packet)) + 512+                            + 128 * toInteger (tmColumnCnt table + length (tmOptionalMetadata table))+                        bytes = fromInteger charge+                        old = maybe 0 fst (M.lookup key (maps current))+                        total = toInteger (mapBytes current) - toInteger old + charge+                        count = M.size (maps current) + if M.member key (maps current) then 0 else 1+                    when (count > maxTableMaps limits || total > toInteger (maxTableMapBytes limits))+                        (throwIO (CDCResourceLimit "CDC table metadata budget exceeded"))+                    writeIORef stateRef current { maps = M.insert key (bytes,table) (maps current), mapBytes = fromInteger total }+                    step+                BINLOG_WRITE_ROWS_EVENTv1 -> row getWriteRowEvent CDCWrite writeFlags+                BINLOG_WRITE_ROWS_EVENTv2 -> row getWriteRowEvent CDCWrite writeFlags+                BINLOG_UPDATE_ROWS_EVENTv1 -> row getUpdateRowEvent CDCUpdate updateFlags+                BINLOG_UPDATE_ROWS_EVENTv2 -> row getUpdateRowEvent CDCUpdate updateFlags+                BINLOG_DELETE_ROWS_EVENTv1 -> row getDeleteRowEvent CDCDelete deleteFlags+                BINLOG_DELETE_ROWS_EVENTv2 -> row getDeleteRowEvent CDCDelete deleteFlags+                MARIADB_GTID_EVENT -> do+                    when (active current /= Nothing || transactionOpen current) (bad "nested/incomplete GTID group")+                    (g,flags,extraFlags) <- getFromBinLogPacket (getGtid (blServerId packet)) packet+                    when (flags .&. 0xc0 /= 0) (throwIO (CDCUnsupportedFeature "XA groups require an XA-aware consumer"))+                    when (extraFlags .&. 0x0e /= 0)+                        (throwIO (CDCUnsupportedFeature "two-phase ALTER requires an ALTER-aware consumer"))+                    when (extraFlags .&. complement 0x1f /= 0)+                        (throwIO (CDCUnsupportedFeature "unknown MariaDB GTID extension flags"))+                    emit (clear current { active = Just (g,flags) }) (CDCBegin g flags) False+                BINLOG_XID_EVENT -> do+                    xid <- getFromBinLogPacket getWord64le packet+                    next <- finish+                    emit next (CDCCommit (Just xid)) True+                BINLOG_QUERY_EVENT -> do+                    query <- getFromBinLogPacket getQueryEvent packet+                    -- Control statements are at most eight bytes. A ninth byte+                    -- proves this is ordinary SQL, so classification need not+                    -- flatten and uppercase a potentially 64 MiB query body.+                    -- Keep the complete original query in the emitted payload.+                    let sql = C.map upper (L.toStrict (L.take 9+                            (L.dropWhile (==32) (fromQuery (qQuery query)))))+                    case sql of+                        "BEGIN" -> emit (clear current { transactionOpen = True }) (CDCQuery query) False+                        "COMMIT" -> finish >>= \next -> emit next (CDCCommit Nothing) True+                        "ROLLBACK" -> finish >>= \next -> emit next CDCRollback True+                        _ -> do+                            let standalone = maybe (not (transactionOpen current)) (\(_,f) -> f .&. 1 /= 0) (active current)+                            next <- if standalone then finish else pure (clear current)+                            emit next (CDCQuery query) standalone+                MARIADB_ANNOTATE_ROWS_EVENT -> emit current (CDCAnnotate (L.toStrict (blBody packet))) False+                BINLOG_ROWS_QUERY_EVENT -> do+                    query <- getFromBinLogPacket getQueryEvent' packet+                    emit current (CDCRowQuery query) False+                BINLOG_ROTATE_EVENT -> do+                    rotation <- getFromBinLogPacket getRotateEvent packet+                    when (transactionOpen current || active current /= Nothing) (bad "rotation inside event group")+                    emit (clear current { filename = B.copy (rFileName rotation) }) (CDCRotate rotation) False+                BINLOG_FORMAT_DESCRIPTION_EVENT -> do+                    fd <- getFromBinLogPacket getFormatDescription packet+                    when (transactionOpen current || active current /= Nothing) (bad "format change inside event group")+                    emit (clear current { format = fd }) (CDCFormat fd) False+                MARIADB_GTID_LIST_EVENT -> do+                    entries <- getFromBinLogPacket getGtidList packet+                    emit current (CDCGtidList entries) False+                MARIADB_BINLOG_CHECKPOINT_EVENT -> do+                    name <- getFromBinLogPacket (getWord32le >>= getByteString . fromIntegral) packet+                    emit current (CDCBinlogCheckpoint name) False+                BINLOG_HEARTBEAT_EVENT -> emit current CDCHeartbeat False+                BINLOG_STOP_EVENT -> do+                    when (active current /= Nothing || transactionOpen current) (bad "STOP inside event group")+                    emit (clear current) CDCStop False+                -- INTVAR/RAND/USER_VAR carry statement context. Expose the raw+                -- event so statement-based consumers cannot lose that context.+                _ | typ `elem` [BINLOG_INTVAR_EVENT,BINLOG_RAND_EVENT,BINLOG_USER_VAR_EVENT,BINLOG_IGNORABLE_EVENT]+                      || blFlags packet .&. 0x80 /= 0 -> emit current (CDCIgnorable packet) False+                  | otherwise -> throwIO (CDCUnsupportedFeature ("non-ignorable event: " ++ show typ))+    failed <- newIORef Nothing+    let guarded = do+            failure <- readIORef failed+            case failure of+                Just err -> throwIO (err :: SomeException)+                Nothing -> step `catch` \err -> writeIORef failed (Just err) >> throwIO (err :: SomeException)+    output <- S.makeInputStream guarded+    pure (output, readIORef latest)+  where+    upper c | c >= 'a' && c <= 'z' = toEnum (fromEnum c - 32)+            | otherwise = c++-- A row's table ID can be inspected without decoding its image. Post-header 6+-- is the old four-byte table ID; ordinary MariaDB headers contain six bytes.+getTableId :: FormatDescription -> BinLogEventType -> Get Word64+getTableId fd typ = do+    tid <- if eventHeaderLen fd typ == 6 then fromIntegral <$> getWord32le else do+        lo <- getWord32le+        hi <- getWord16le+        pure (fromIntegral lo .|. (fromIntegral hi `shiftL` 32))+    void getRemainingLazyByteString+    pure tid++-- MariaDB pads short GTIDs to 19 body bytes, or appends a group-commit ID and+-- typed extensions. Known neutral engine/thread metadata may be skipped only+-- after its complete layout is validated. XA/two-phase DDL are rejected by the+-- caller before delivery; unknown extension flags must never imply safety.+getGtid :: Word32 -> Get (Gtid,Word8,Word8)+getGtid server = do+    start <- bytesRead+    sequenceNo <- getWord64le+    domain <- getWord32le+    flags <- getWord8+    when (sequenceNo == 0) (fail "zero GTID sequence")+    when (flags .&. 2 /= 0) (void getWord64le)+    extraFlags <- if flags .&. 0xc0 /= 0 then do+        -- The XA layout is intentionally not interpreted as ordinary metadata.+        void getRemainingLazyByteString+        pure 0+      else do+        empty <- isEmpty+        extra <- if empty then pure 0 else getWord8+        if extra .&. complement 0x1f /= 0 then void getRemainingLazyByteString+          else do+            when (extra .&. 1 /= 0) $ do+                engines <- getWord8+                when (engines == 0) (fail "zero additional GTID engine count")+            when (extra .&. 0x0c /= 0) (void getWord64le)+            when (extra .&. 0x10 /= 0) (void getWord32le)+            padding <- getRemainingLazyByteString+            unless (L.all (==0) padding) (fail "nonzero GTID padding")+        pure extra+    end <- bytesRead+    when (end - start < 19) (fail "truncated MariaDB GTID header")+    pure (Gtid domain server sequenceNo, flags, extraFlags)++getGtidList :: Get GtidList+getGtidList = do+    countAndFlags <- getWord32le+    let count = countAndFlags .&. 0x0fffffff+    when (count > 4096) (fail "GTID list exceeds entry budget")+    entries <- replicateM (fromIntegral count) (Gtid <$> getWord32le <*> getWord32le <*> getWord64le)+    let identities = M.fromList [((gtidDomainId g,gtidServerId g),()) | g <- entries]+    when (any ((==0) . gtidSequence) entries || M.size identities /= length entries)+        (fail "invalid/duplicate GTID history entry")+    pure (GtidList (countAndFlags .&. 0xf0000000) entries)++advance :: Gtid -> GtidSet -> IO GtidSet+advance g (GtidSet entries) = do+    when (maybe False ((>= gtidSequence g) . gtidSequence) (M.lookup (gtidDomainId g) entries))+        (bad "non-advancing GTID sequence")+    when (M.size entries >= 4096 && M.notMember (gtidDomainId g) entries) (throwIO (CDCResourceLimit "GTID domain budget exceeded"))+    pure (GtidSet (M.insert (gtidDomainId g) g entries))++bad :: String -> IO a+bad = throwIO . CDCDecodeError++-- Compressed events leave their post-header and bitmaps uncompressed. Only+-- the row/query payload has MariaDB's algorithm/length prefix followed by zlib.+-- Incremental inflation enforces both the advertised length and our expansion+-- budget before retaining output, including on a deliberately tiny zip bomb.+expandEvent :: DecodeLimits -> FormatDescription -> BinLogPacket -> IO BinLogPacket+expandEvent limits fd packet+    | L.length (blBody packet) > fromIntegral (maxExpandedEventBytes limits) = throwIO (CDCResourceLimit "event byte budget exceeded")+    | Just normal <- lookup (blEventType packet) compressedTypes = do+        (prefix, compressed) <- getFromBinLogPacket (splitCompressed normal) packet+        (size, payload) <- getFromBinLogPacket compressedHeader packet { blBody = compressed }+        when (size <= 0 || toInteger size + toInteger (L.length prefix) > toInteger (maxExpandedEventBytes limits))+            (throwIO (CDCResourceLimit "compressed event expansion budget exceeded"))+        inflated <- inflateBounded size payload+        pure packet { blEventType = normal, blBody = prefix <> inflated }+    | otherwise = pure packet+  where+    splitCompressed normal = do+        start <- bytesRead+        if normal == BINLOG_QUERY_EVENT then do+            skip 8+            schemaLength <- getWord8+            skip 2+            statusLength <- getWord16le+            skip (fromIntegral statusLength + fromIntegral schemaLength + 1)+          else do+            skip (if eventHeaderLen fd normal == 6 then 6 else 8)+            when (normal `elem` [BINLOG_WRITE_ROWS_EVENTv2,BINLOG_UPDATE_ROWS_EVENTv2,BINLOG_DELETE_ROWS_EVENTv2]) $ do+                extra <- getWord16le+                when (extra < 2) (fail "compressed rows: invalid extra header")+                skip (fromIntegral extra - 2)+            columns <- getLenEncInt+            when (columns <= 0 || columns > 4096) (fail "compressed rows: invalid column count")+            skip (((columns + 7) `div` 8) * if normal `elem` [BINLOG_UPDATE_ROWS_EVENTv1,BINLOG_UPDATE_ROWS_EVENTv2] then 2 else 1)+        end <- bytesRead+        remainder <- getRemainingLazyByteString+        pure (L.take (end - start) (blBody packet), remainder)+    compressedHeader = do+        header <- getWord8+        let width = fromIntegral (header .&. 7)+        unless (header .&. 0xf8 == 0x80 && width >= 1 && width <= 4)+            (fail "unsupported MariaDB binlog compression header")+        bytes <- getByteString width+        let size = B.foldl' (\n b -> n * 256 + fromIntegral b) (0 :: Word64) bytes+        when (size > fromIntegral (maxBound :: Int)) (fail "compressed length overflow")+        payload <- getRemainingLazyByteString+        pure (fromIntegral size, payload)++compressedTypes :: [(BinLogEventType,BinLogEventType)]+compressedTypes = [(MARIADB_QUERY_COMPRESSED_EVENT,BINLOG_QUERY_EVENT),+    (MARIADB_WRITE_ROWS_COMPRESSED_EVENTv1,BINLOG_WRITE_ROWS_EVENTv1),+    (MARIADB_UPDATE_ROWS_COMPRESSED_EVENTv1,BINLOG_UPDATE_ROWS_EVENTv1),+    (MARIADB_DELETE_ROWS_COMPRESSED_EVENTv1,BINLOG_DELETE_ROWS_EVENTv1),+    (MARIADB_WRITE_ROWS_COMPRESSED_EVENTv2,BINLOG_WRITE_ROWS_EVENTv2),+    (MARIADB_UPDATE_ROWS_COMPRESSED_EVENTv2,BINLOG_UPDATE_ROWS_EVENTv2),+    (MARIADB_DELETE_ROWS_COMPRESSED_EVENTv2,BINLOG_DELETE_ROWS_EVENTv2)]++inflateBounded :: Int -> L.ByteString -> IO L.ByteString+inflateBounded expected input = go 0 [] (L.toChunks input) (Z.decompressIO Z.zlibFormat Z.defaultDecompressParams)+  where+    go !size chunks pending stream = case stream of+        Z.DecompressInputRequired next -> case pending of+            [] -> next B.empty >>= go size chunks []+            chunk:rest -> next chunk >>= go size chunks rest+        Z.DecompressOutputAvailable chunk next -> do+            let total = size + B.length chunk+            when (total > expected) (throwIO (CDCResourceLimit "inflated event exceeds advertised length"))+            next >>= go total (chunk:chunks) pending+        Z.DecompressStreamEnd trailing -> do+            unless (size == expected && B.null trailing && all B.null pending)+                (bad "compressed event length/trailing data mismatch")+            pure (L.fromChunks (reverse chunks))+        Z.DecompressStreamError err -> bad ("invalid zlib event: " ++ show err)++-- MariaDB 13.2 can split one row event into type-172 fragments. All physical+-- fragments have already passed CRC validation. Reassemble only the one bounded+-- event; reject interleaving, missing/repeated fragments and false size claims.+assembleEvents :: DecodeLimits -> S.InputStream BinLogPacket -> IO (S.InputStream BinLogPacket)+assembleEvents limits input = S.makeInputStream $ do+    next <- S.read input+    case next of+        Just packet | blEventType packet == MARIADB_PARTIAL_ROW_DATA_EVENT -> do+            (total, sequenceNo, flags, size, first) <- getFromBinLogPacket fragment packet+            unless (total >= 2 && total <= 65536 && sequenceNo == 1 && flags == 1+                    && size >= 19 && size <= fromIntegral (maxExpandedEventBytes limits))+                (bad "invalid first row fragment/expansion budget")+            gather total 2 size (L.length first) [first] (blSemiAck packet) packet+        other -> pure other+  where+    fragment = do+        total <- getWord32le+        sequenceNo <- getWord32le+        flags <- getWord8+        size <- if flags .&. 1 /= 0 then getWord64le else pure 0+        body <- getRemainingLazyByteString+        pure (total, sequenceNo, flags, size, body)+    gather total expected size used chunks ack previous+        | used > fromIntegral size = bad "row fragments exceed advertised event size"+        | expected > total = do+            unless (used == fromIntegral size) (bad "incomplete reassembled row event")+            let body = L.concat (reverse chunks)+            -- The embedded event's on-disk size may still include its checksum;+            -- fragments carry their own checksums instead. Decode the common+            -- header directly and require an ordinary row event.+            embedded <- getFromBinLogPacket embeddedRow previous { blBody = body }+            unless (fromIntegral (blEventSize embedded) == used || fromIntegral (blEventSize embedded) == used + 4)+                (bad "embedded row event size mismatch")+            unless (blEventType embedded `elem` [BINLOG_WRITE_ROWS_EVENTv1,BINLOG_UPDATE_ROWS_EVENTv1,+                    BINLOG_DELETE_ROWS_EVENTv1,BINLOG_WRITE_ROWS_EVENTv2,BINLOG_UPDATE_ROWS_EVENTv2,BINLOG_DELETE_ROWS_EVENTv2])+                (bad "fragmented payload is not a supported row event")+            pure (Just embedded { blLogPos = blLogPos previous, blSemiAck = ack })+        | otherwise = do+            packet <- S.read input >>= maybe (bad "binlog ended inside fragmented row event") pure+            unless (blEventType packet == MARIADB_PARTIAL_ROW_DATA_EVENT && blServerId packet == blServerId previous) (bad "interleaved row fragment stream")+            (count, sequenceNo, flags, _, chunk) <- getFromBinLogPacket fragment packet+            unless (count == total && sequenceNo == expected && flags == 0 && not (L.null chunk))+                (bad "out-of-order/invalid row fragment")+            gather total (expected + 1) size (used + L.length chunk) (chunk:chunks) (ack || blSemiAck packet) packet+    embeddedRow = BinLogPacket <$> getWord32le <*> (toEnum . fromIntegral <$> getWord8)+        <*> getWord32le <*> getWord32le <*> (fromIntegral <$> getWord32le)+        <*> getWord16le <*> getRemainingLazyByteString <*> pure False
+ src/Database/MySQL/Connection.hs view
@@ -0,0 +1,1933 @@+{-# LANGUAGE ScopedTypeVariables #-}++{-|+Module      : Database.MySQL.Connection+Description : Internal MariaDB transport and connection lifecycle+Copyright   : (c) Winterland, 2016+License     : BSD+Maintainer  : Anton Antich <anton.antich@gmail.com>+Stability   : experimental+Portability : macOS and Linux tested++Applications should use the abstract t'MySQLConn' exported by+"Database.MySQL.Base". This exposed internal module also supports deterministic+transport fixtures; modifying its records bypasses the public safety contract.++-}++module Database.MySQL.Connection where++import           Control.Exception hiding (handle)+import           Control.Monad+import qualified Crypto.ECC.Edwards25519         as Edwards+import qualified Crypto.PubKey.Ed25519           as Ed25519+import qualified Crypto.KDF.PBKDF2               as PBKDF2+import           Crypto.Error                    (throwCryptoError)+import           Crypto.Random                   (getRandomBytes)+import qualified Crypto.Hash                     as Crypto+import           Crypto.Hash.Algorithms          (SHA1(..))+import qualified Crypto.Number.Serialize         as Serialize+import qualified Crypto.PubKey.RSA               as RSA+import qualified Crypto.PubKey.RSA.OAEP          as OAEP+import qualified Data.ASN1.BinaryEncoding        as ASN1B+import qualified Data.ASN1.BitArray              as ASN1BA+import qualified Data.ASN1.Encoding              as ASN1E+import qualified Data.ASN1.Types                 as ASN1+import qualified Data.Binary                     as Binary+import qualified Data.Binary.Put                 as Binary+import           Data.Bits+import qualified Data.ByteArray                  as BA+import           Data.ByteString                 (ByteString)+import qualified Data.ByteString                 as B+import qualified Data.ByteString.Lazy            as L+import           Data.IORef                      (IORef, newIORef, readIORef, atomicModifyIORef',+                                                  writeIORef)+import qualified Data.PEM                        as PEM+import           Data.Typeable+import           Data.Word+import           Database.MySQL.Protocol.Auth+import           Database.MySQL.Authentication+import qualified Database.MySQL.Authentication.GSSAPI as GSSAPI+import           Database.MySQL.Protocol.Command+import           Database.MySQL.Protocol.Packet+import           Network.Socket                  (HostName, PortNumber)+import qualified Network.Socket                  as N+import           System.IO.Streams               (InputStream)+import qualified System.IO.Streams               as Stream+import qualified System.IO.Streams.TCP           as TCP+import qualified Data.Connection                 as Conn++--------------------------------------------------------------------------------++import Control.Concurrent (ThreadId, myThreadId)+import System.Timeout (timeout)+import qualified Data.Binary.Parser as P+import qualified Data.ByteString.Char8 as BC+import qualified Data.Text.Encoding as TE+import qualified Data.Text as T+import qualified Data.Vector as V+import Data.Int (Int64)+import Data.Maybe (isJust)+import qualified Data.IntMap.Strict as IM+import Database.MySQL.Session+import Database.MySQL.Protocol.ColumnDef+import Database.MySQL.Protocol.MySQLValue+import Database.MySQL.Protocol.Progress+import Database.MySQL.Protocol.Compression+import Database.MySQL.Query++-- | Per-connection resource bounds. Limits are validated before opening a+-- transport; none can be zero. Message limits include continuation fragments.+data ConnectionOptions = ConnectionOptions+    { maximumMessageBytes :: !Word64 -- ^ Maximum inbound/outbound logical message.+    , maximumMetadataBytes :: !Word64 -- ^ Per-metadata-block and OK tracking budget.+    , maximumColumns :: !Int -- ^ Maximum fields in a result or statement metadata block.+    , cleanupTimeoutMicros :: !Int -- ^ One deadline for draining and rollback.+    , connectTimeoutMicros :: !Int -- ^ Whole establishment deadline: DNS, TLS, authentication and setup.+    , maximumAuthIterations :: !Int -- ^ Maximum server-requested PARSEC PBKDF2 work.+    , enableSessionTracking :: !Bool -- ^ Negotiate tracking and OK terminators together.+    , enableMetadataCaching :: !Bool -- ^ Negotiate MariaDB prepared metadata omission.+    , enableExtendedMetadata :: !Bool -- ^ Retain native type/format attributes.+    , maximumPreparedStatements :: !Int -- ^ Maximum registered server statements.+    , maximumBulkRows :: !Int -- ^ Rows per bulk command; batches are never silently split.+    , maximumLongDataBytes :: !Word64 -- ^ Total streamed parameter bytes per execution.+    , maximumCursorFetchRows :: !Word32 -- ^ Maximum requested rows in one server cursor fetch.+    , enableProgressReporting :: !Bool -- ^ Negotiate reports; install callbacks separately.+    , enableCompression :: !Bool -- ^ Negotiate zlib transport framing; off by default.+    , compressionThresholdBytes :: !Int -- ^ Try compression for outbound frames at least this large.+    } deriving (Eq, Show)++-- | 64 MiB messages, 16 MiB metadata, 65,535 columns, one-second cleanup,+-- ten-second establishment, at most 2^20 PARSEC iterations and session tracking.+defaultConnectionOptions :: ConnectionOptions+defaultConnectionOptions = ConnectionOptions+    { maximumMessageBytes = 64 * 1024 * 1024+    , maximumMetadataBytes = 16 * 1024 * 1024+    , maximumColumns = 65535+    , cleanupTimeoutMicros = 1000000+    , connectTimeoutMicros = 10000000+    , maximumAuthIterations = 1048576+    , enableSessionTracking = True+    , enableMetadataCaching = True+    , enableExtendedMetadata = True+    , maximumPreparedStatements = 1024+    , maximumBulkRows = 10000+    , maximumLongDataBytes = 64 * 1024 * 1024+    , maximumCursorFetchRows = 65536+    , enableProgressReporting = False+    , enableCompression = False+    , compressionThresholdBytes = 1024+    }++-- A cached fact must not pin a large result buffer or grow without bound.+-- Startup snapshots and subsequent tracker deltas share this byte limit.+maximumRenderingValueBytes :: Int+maximumRenderingValueBytes = 4096++validateOptions :: ConnectionOptions -> IO ()+validateOptions options = unless+    (maximumMessageBytes options > 0 && maximumMessageBytes options <= fromIntegral (maxBound :: Word32)+      && maximumMessageBytes options <= fromIntegral (maxBound :: Int)+      && maximumMetadataBytes options > 0 && maximumColumns options > 0 && cleanupTimeoutMicros options > 0+      && connectTimeoutMicros options > 0 && maximumAuthIterations options >= 1024+      && maximumAuthIterations options <= 1073741824+      && maximumPreparedStatements options > 0 && maximumBulkRows options > 0+      && maximumLongDataBytes options > 0 && maximumCursorFetchRows options > 0+      && compressionThresholdBytes options >= 0 && compressionThresholdBytes options <= 65536)+    (throwIO (ProtocolError "invalid connection resource options"))++-- | Observable lifecycle. Only 'Ready' admits a new command. 'Broken' and+-- 'Closed' are terminal; an unfinished stream retains its creating thread.+data ConnectionStatus = Ready | Executing | Streaming | Replicating | Broken | Closed deriving (Eq, Show)+-- Completion must be distinct from "bytes were sent": a concurrent close after+-- a terminal COMMIT reply does not make a known server error uncertain.+data ExchangeProgress = BeforeSend | WireInFlight | ReplyComplete deriving (Eq,Show)+data Runtime = Runtime+    { runtimeStatus :: !ConnectionStatus+    , runtimeGeneration :: !Word64+    , runtimeOwner :: !(Maybe ThreadId)+    , transactionOwner :: !(Maybe ThreadId)+    } deriving Show+-- | Bounded diagnostics, deliberately excluding SQL, parameters and credentials.+data ConnectionFailure = ConnectionFailure+    { failurePhase :: !ConnectionStatus -- ^ Phase in which the connection was retired.+    , commitOutcomeUncertain :: !Bool -- ^ COMMIT may have reached MariaDB without a reply.+    , secondaryCleanupFailure :: !Bool -- ^ Cleanup also failed after a primary exception.+    } deriving (Eq, Show)+-- | Local ownership, resource, session-context and transaction failures.+-- 'CommitOutcomeUnknown' retains the original synchronous exception; inspect+-- connection diagnostics for uncertain commits interrupted by cancellation.+data ConnectionError = ConnectionUnavailable ConnectionStatus | ConcurrentConnectionUse | StaleResultStream+                     | NestedTransactionUnsupported | SessionContextUnavailable | ProtocolError String+                     | CommitOutcomeUnknown SomeException | CleanupDeadlineExceeded+    deriving (Typeable, Show)+instance Exception ConnectionError++-- | A MariaDB session with one command/stream owner at a time. Obtain it through+-- the connection constructors and close it with 'close'. The public Base module+-- keeps this representation abstract; direct internal record access bypasses+-- the ownership, resource and session-state contracts.+data MySQLConn = MySQLConn+    { mysqlRead :: !(InputStream Packet)+    , mysqlWrite :: !(Packet -> IO ())+    , mysqlCloseSocket :: !(IO ())+    , connectionOptions :: !ConnectionOptions+    , connectionRuntime :: !(IORef Runtime)+    , connectionFailureRef :: !(IORef (Maybe ConnectionFailure))+    , connectionServerStatus :: !(IORef (Maybe Word16))+    , connectionRenderContext :: !(IORef (Maybe RenderContext))+    , connectionCapabilities :: !(IORef Word64)+    , connectionSequence :: !(IORef FramingState)+    , connectionExchangeProgress :: !(IORef ExchangeProgress)+    , connectionReleased :: !(IORef Bool)+    , connectionDrain :: !(IORef (Maybe (IO ())))+    , connectionServerPacketLimit :: !(IORef Word64)+    , connectionQueryEffect :: !(IORef (Maybe Bool))+    , connectionOpaqueStatements :: !(IORef [Word32])+    , connectionStatements :: !(IORef (Maybe StatementRegistry))+    , connectionStatementSerial :: !(IORef Word64)+    , connectionActiveStatement :: !(IORef (Maybe StmtID))+    , connectionResultIndex :: !(IORef Int)+    , connectionSession :: !(IORef SessionCache)+    , connectionInitialSession :: !(IORef (Maybe InitialSession))+    , connectionProgressCallback :: !(IORef (Maybe (Progress -> IO ())))+    , connectionCallbackActive :: !(IORef Bool)+    , connectionCompressionStart :: !(IO ())+    , connectionCompressionReset :: !(IO ())+    , connectionCompressionAllowReplicationResets :: !(IO ())+    }++-- A decoder is compiled per metadata shape, not per row/execution. Metadata+-- cannot be evicted independently: MariaDB may omit it in the next response.+-- The aggregate byte charge shares the connection's metadata resource budget.+data CachedColumns = CachedColumns+    { cachedDetails :: ![ColumnMetadata]+    , cachedFields :: ![ColumnDef]+    , cachedColumnCount :: !Int+    -- Intentionally lazy: ordinary text results must not construct binary+    -- field parsers. The first binary read forces and shares this decoder.+    , cachedBinaryDecoder :: Binary.Get [MySQLValue]+    , cachedMetadataBytes :: !Word64+    }++data StatementState = StatementState+    { statementSerial :: !Word64+    , statementArity :: !Int+    , statementResults :: !(IM.IntMap CachedColumns)+    , statementMetadataBytes :: !Word64 -- ^ Sum of this statement's cached blocks.+    }++-- | Keep byte charges beside the entries they describe, under one atomic update.+-- A metadata refresh must not scan unrelated statements: disabling omission or+-- changing a parameter type can put this operation on every execution's path.+data StatementRegistry = StatementRegistry+    { registryStatements :: !(IM.IntMap StatementState)+    , registryMetadataBytes :: !Word64+    }++-- | Read lifecycle diagnostics without claiming a command or performing I/O.+connectionStatus :: MySQLConn -> IO ConnectionStatus+connectionStatus = fmap runtimeStatus . readIORef . connectionRuntime+-- | Return retirement diagnostics, or 'Nothing' if no failure has been recorded.+connectionFailure :: MySQLConn -> IO (Maybe ConnectionFailure)+connectionFailure = readIORef . connectionFailureRef++-- Shared construction is also used by deterministic transport fixtures.+newConnection :: ConnectionOptions -> InputStream Packet -> (Packet -> IO ()) -> IO () -> IO MySQLConn+newConnection options input output abort = newConnectionUsing options makeReader output abort+  where+    makeReader framing = Stream.makeInputStream $ do+        packet <- Stream.read input+        case packet of+            Nothing -> pure Nothing+            Just value -> advanceFraming (maximumMessageBytes options) framing (pLen value) (pSeqN value) >> pure (Just value)++-- Live sockets validate framing in the byte decoder itself, without another+-- InputStream wrapper and another read/Maybe allocation for each physical row.+newByteConnection :: ConnectionOptions -> InputStream ByteString -> (Packet -> IO ()) -> IO () -> IO MySQLConn+newByteConnection options input output abort = newConnectionUsing options (\framing -> decodeFramedInput (maximumMessageBytes options) framing input) output abort++-- Raw transports share one optional byte layer; ordinary connections retain+-- their existing reader/writer path without compression checks per packet.+newTransportConnection :: ConnectionOptions -> InputStream ByteString -> (L.ByteString -> IO ()) -> IO () -> IO MySQLConn+newTransportConnection = buildTransportConnection (\conn _ -> pure conn)++-- | Construct two views of one transport. Authentication rejects oversized+-- physical headers before reading their bodies. After authentication, the+-- ordinary view uses the caller's full message budget without an extra mutable+-- phase check on every query packet. Both views share framing and unread bytes.+newAuthenticatingTransportConnection :: ConnectionOptions -> InputStream ByteString -> (L.ByteString -> IO ()) -> IO () -> IO (MySQLConn, MySQLConn)+newAuthenticatingTransportConnection = buildTransportConnection $ \conn input -> do+    packets <- decodeFramedInput (authenticationWireLimit (connectionOptions conn))+        (connectionSequence conn) input+    pure (conn {mysqlRead=packets}, conn)++-- Select compression once; both connection views must read its same byte stream.+buildTransportConnection :: (MySQLConn -> InputStream ByteString -> IO a) -> ConnectionOptions -> InputStream ByteString -> (L.ByteString -> IO ()) -> IO () -> IO a+buildTransportConnection finish options input output abort+    | not (enableCompression options) = do+        conn <- newByteConnection options input (output . Binary.encode) abort+        finish conn input+    | otherwise = do+        transport <- newCompressionTransport (maximumMessageBytes options + 4)+            (compressionThresholdBytes options) input output+        conn <- newByteConnection options (compressionInput transport)+            (compressionSend transport . Binary.encode) abort+        finish conn {connectionCompressionStart = compressionStart transport,+                   connectionCompressionReset = compressionReset transport,+                   connectionCompressionAllowReplicationResets = compressionAllowReplicationResets transport}+            (compressionInput transport)++-- | One MiB of plugin data plus the largest accepted AuthSwitch envelope+-- (marker, 128-byte plugin name and NUL). Caller budgets may be smaller. This+-- phase limit also covers the greeting and terminal authentication messages.+authenticationWireLimit :: ConnectionOptions -> Word64+authenticationWireLimit options = min (maximumMessageBytes options) (1048576 + 130)++newConnectionUsing :: ConnectionOptions -> (IORef FramingState -> IO (InputStream Packet)) -> (Packet -> IO ()) -> IO () -> IO MySQLConn+newConnectionUsing options makeReader output abort = do+    validateOptions options+    state <- newIORef (Runtime Ready 0 Nothing Nothing)+    failure <- newIORef Nothing+    status <- newIORef Nothing+    context <- newIORef Nothing+    caps <- newIORef 0+    framing <- newIORef (FramingState 0 0)+    started <- newIORef BeforeSend+    released <- newIORef False+    drain <- newIORef Nothing+    serverLimit <- newIORef (maximumMessageBytes options)+    effect <- newIORef Nothing+    opaque <- newIORef []+    statements <- newIORef (Just (StatementRegistry IM.empty 0))+    serial <- newIORef 0+    active <- newIORef Nothing+    resultIndex <- newIORef 0+    observations <- newIORef (emptySessionCache 0)+    initial <- newIORef Nothing+    progressCallback <- newIORef Nothing+    callbackActive <- newIORef False+    input <- makeReader framing+    let checkedOutput packet = advanceFraming (maximumMessageBytes options) framing (pLen packet) (pSeqN packet) >> output packet+    pure MySQLConn+        { mysqlRead = input+        , mysqlWrite = checkedOutput+        , mysqlCloseSocket = abort+        , connectionOptions = options+        , connectionRuntime = state+        , connectionFailureRef = failure+        , connectionServerStatus = status+        , connectionRenderContext = context+        , connectionCapabilities = caps+        , connectionSequence = framing+        , connectionExchangeProgress = started+        , connectionReleased = released+        , connectionDrain = drain+        , connectionServerPacketLimit = serverLimit+        , connectionQueryEffect = effect+        , connectionOpaqueStatements = opaque+        , connectionStatements = statements+        , connectionStatementSerial = serial+        , connectionActiveStatement = active+        , connectionResultIndex = resultIndex+        , connectionSession = observations+        , connectionInitialSession = initial+        , connectionProgressCallback = progressCallback+        , connectionCallbackActive = callbackActive+        , connectionCompressionStart = pure ()+        , connectionCompressionReset = pure ()+        , connectionCompressionAllowReplicationResets = pure ()+        }++-- Retain only restoration facts, never ConnectInfo or authentication material.+data InitialSession = InitialSession+    { initialSchema :: !ByteString+    , initialCharset :: !ByteString+    , initialCollation :: !ByteString+    , initialVersion :: !Int+    , initialRole :: !(Maybe ByteString)+    }++data FramingState = FramingState {-# UNPACK #-} !Word16 {-# UNPACK #-} !Word64+-- The 257 possible empty states are constants, shared by every connection.+-- Ordinary short packets need no new framing-state allocation per row.+emptyFramingStates :: V.Vector FramingState+emptyFramingStates=V.generate 257 (\n -> FramingState (fromIntegral n) 0)+{-# NOINLINE emptyFramingStates #-}+-- 256 permits physical packet-number resets in replication and compressed+-- exchanges. Logical continuations still use the assembler's modulo-256 check.+setExchangeSequence :: MySQLConn -> Word16 -> IO ()+setExchangeSequence conn sequenceN = do+    -- MariaDB net_flush synchronizes ordinary pkt_nr to compress_pkt_nr.+    -- Thus ordinary packet numbers may jump at compressed flush boundaries;+    -- Connector/C also validates only compressed-frame sequence in this mode.+    -- Logical continuation ordering remains checked by readPacketWithLimit.+    effective <- if sequenceN == 0 && enableCompression (connectionOptions conn)+        then do+            caps <- readIORef (connectionCapabilities conn)+            pure (if testBit caps 5 then 256 else sequenceN)+        else pure sequenceN+    writeIORef (connectionSequence conn) (FramingState effective 0)++advanceFraming :: Word64 -> IORef FramingState -> Int64 -> Word8 -> IO ()+advanceFraming limit ref lengthBytes sequenceN = do+    FramingState expected accumulated <- readIORef ref+    unless (expected==256 || expected==fromIntegral sequenceN)+        (throwIO (ProtocolError ("unexpected packet sequence: expected " ++ show expected ++ ", received " ++ show sequenceN)))+    when (lengthBytes<0 || lengthBytes>16777215) (throwIO (ProtocolError "invalid physical packet length"))+    let bytes=fromIntegral lengthBytes+    when (accumulated>limit || bytes>limit-accumulated) (throwIO (ProtocolError "logical packet exceeds client limit"))+    let next=if expected==256 then 256 else (expected+1) .&. 255+        continued=if lengthBytes==16777215 then accumulated+bytes else 0+    writeIORef' ref (if continued==0 then V.unsafeIndex emptyFramingStates (fromIntegral next) else FramingState next continued)+{-# INLINE advanceFraming #-}++-- Claim the finalizer once so close, cancellation and a failed stream cannot+-- double-close a transport. Transport finalizers must release resources even+-- if their graceful TLS/socket shutdown fails.+releaseTransport :: MySQLConn -> IO ()+releaseTransport conn = do+    already <- atomicModifyIORef' (connectionReleased conn) (\x -> (True,x))+    unless already (mysqlCloseSocket conn)++abortConnection :: MySQLConn -> Bool -> IO ()+abortConnection conn uncertain = mask_ $ do+    clearStatements conn+    writeIORef (connectionProgressCallback conn) Nothing+    invalidateSession conn+    phase <- atomicModifyIORef' (connectionRuntime conn) $ \rt ->+        (rt { runtimeStatus=if runtimeStatus rt == Closed then Closed else Broken, runtimeOwner=Nothing },runtimeStatus rt)+    atomicModifyIORef' (connectionFailureRef conn) $ \prior ->+        let failure=case prior of+                Nothing -> ConnectionFailure phase uncertain False+                Just previous -> previous {commitOutcomeUncertain=commitOutcomeUncertain previous || uncertain}+        in (Just failure,())+    writeIORef (connectionRenderContext conn) Nothing+    writeIORef (connectionDrain conn) Nothing+    -- An ordinary close error must not replace the original protocol failure.+    -- A new cancellation still belongs to the caller and must propagate.+    releaseTransport conn `catch` (\(err :: SomeException) -> when (isCancellation err) (throwIO err))++-- The atomic claim rejects misuse instead of queueing commands. The surrounding+-- mask in withCommand closes the cancellation gap between claiming and setup.+claimCommand :: MySQLConn -> IO ()+claimCommand conn = do+    tid <- myThreadId+    result <- atomicModifyIORef' (connectionRuntime conn) $ \rt ->+        let unavailable err = (rt,Left err) in+        if maybe False (/=tid) (transactionOwner rt) then unavailable ConcurrentConnectionUse+        else case runtimeStatus rt of+            Ready -> (rt {runtimeStatus=Executing,runtimeGeneration=runtimeGeneration rt+1,runtimeOwner=Just tid},Right ())+            Streaming -> unavailable (ConnectionUnavailable Streaming)+            Executing -> unavailable ConcurrentConnectionUse+            other -> unavailable (ConnectionUnavailable other)+    case result of+        Left (ConnectionUnavailable Streaming) -> throwIO UnconsumedResultSet+        Left err -> throwIO err+        Right () -> do+            writeIORef (connectionExchangeProgress conn) BeforeSend+            writeIORef (connectionQueryEffect conn) Nothing++-- Finalize every exchange-local reference before publishing Ready. Masking+-- protects this thread from cancellation; it does not exclude another thread+-- from claiming Ready immediately after the atomic runtime update.+finishResponse :: MySQLConn -> IO ()+finishResponse conn = mask_ $ do+    writeIORef (connectionDrain conn) Nothing+    caps <- readIORef (connectionCapabilities conn)+    started <- readIORef (connectionExchangeProgress conn)+    writeIORef (connectionExchangeProgress conn) ReplyComplete+    effect <- readIORef (connectionQueryEffect conn)+    when (started/=BeforeSend && (caps .&. 0x800000 == 0 || effect==Just True)) $ do+        writeIORef (connectionRenderContext conn) Nothing+        -- Preserve the latest actual envelope for observers, while declining+        -- to present cached facts as current after an opaque effect.+        invalidateSessionFacts conn+    atomicModifyIORef' (connectionRuntime conn) $ \rt ->+        (if runtimeStatus rt `elem` [Executing,Streaming] then rt {runtimeStatus=Ready,runtimeOwner=Nothing} else rt,())++-- A pre-send validation failure releases the claim. Once writing may have+-- started, any incomplete reply retires the connection rather than guessing+-- whether the next packet belongs to this command or the next one.+withCommand :: MySQLConn -> IO a -> IO a+withCommand conn action = mask $ \restore -> do+    claimCommand conn+    generation <- runtimeGeneration <$> readIORef (connectionRuntime conn)+    result <- restore action `catch` \(err :: SomeException) -> do+        runtime <- readIORef (connectionRuntime conn)+        started <- readIORef (connectionExchangeProgress conn)+        -- The action may have completed its reply before throwing. A successor+        -- can already own Ready, so finalizers may touch only their generation.+        when (runtimeGeneration runtime == generation && runtimeStatus runtime `elem` [Executing,Streaming,Replicating]) $+            if started==WireInFlight then abortConnection conn False else finishResponse conn+        throwIO err+    runtime <- readIORef (connectionRuntime conn)+    when (runtimeGeneration runtime == generation && runtimeStatus runtime == Executing) (finishResponse conn)+    pure result++-- A completed stream must never read from a later exchange. Check generation+-- before ownership so stale handles fail without consuming a single packet.+streamGuard :: MySQLConn -> Word64 -> IO a -> IO a+streamGuard conn generation action = do+    when (enableProgressReporting (connectionOptions conn)) $ do+        callbackActive <- readIORef (connectionCallbackActive conn)+        when callbackActive (throwIO ConcurrentConnectionUse)+    rt <- readIORef (connectionRuntime conn)+    tid <- myThreadId+    unless (runtimeGeneration rt == generation) (throwIO StaleResultStream)+    unless (runtimeStatus rt `elem` [Streaming,Replicating]) (throwIO (ConnectionUnavailable (runtimeStatus rt)))+    unless (runtimeOwner rt == Just tid) (throwIO ConcurrentConnectionUse)+    action `catch` \(err :: SomeException) -> do+        current <- readIORef (connectionRuntime conn)+        when (runtimeGeneration current == generation && runtimeStatus current `elem` [Streaming,Replicating])+            (abortConnection conn False)+        throwIO err++markStreaming :: MySQLConn -> IO Word64+markStreaming conn = markResponseStream conn Streaming++-- A concurrent close is terminal, including the gap between metadata receipt+-- and stream construction. Never overwrite Closed/Broken with a stream phase.+markResponseStream :: MySQLConn -> ConnectionStatus -> IO Word64+markResponseStream conn phase = do+    result <- atomicModifyIORef' (connectionRuntime conn) $ \rt ->+        if runtimeStatus rt == Executing+        then (rt {runtimeStatus=phase}, Right (runtimeGeneration rt))+        else (rt, Left (ConnectionUnavailable (runtimeStatus rt)))+    either throwIO pure result++beginTransactionLease :: MySQLConn -> IO ()+beginTransactionLease conn = do+    tid <- myThreadId+    result <- atomicModifyIORef' (connectionRuntime conn) $ \rt ->+        if isJust (transactionOwner rt) then (rt,False) else (rt {transactionOwner=Just tid},True)+    unless result (throwIO NestedTransactionUnsupported)++endTransactionLease :: MySQLConn -> IO ()+endTransactionLease conn = atomicModifyIORef' (connectionRuntime conn) (\rt -> (rt {transactionOwner=Nothing},()))++-- | Endpoint, credentials and initial one-byte protocol collation ID. The+-- 'Show' instance always redacts the password, including an empty password.+data ConnectInfo = ConnectInfo+    { ciHost :: HostName+      -- ^ TCP address; TLS certificate identity is passed separately.+    , ciPort :: PortNumber+      -- ^ TCP port (default 3306); ignored by Unix-socket constructors.+    , ciDatabase :: ByteString+      -- ^ Initial schema bytes; choose an explicit schema for pooled reset.+    , ciUser :: ByteString+      -- ^ MariaDB account name, interpreted according to server authentication.+    , ciPassword :: ByteString+      -- ^ Password bytes, excluded from the diagnostic 'Show' representation.+    , ciCharset :: Word8+      -- ^ Handshake collation ID; defaults to utf8mb4_unicode_ci (224).+    }++-- Preserve ordinary record-style diagnostics without ever evaluating or+-- rendering credential bytes. Do not implement this by replacing a substring+-- in derived output: quoting/escaping or overlapping fields could leak secrets.+instance Show ConnectInfo where+    showsPrec precedence ci = showParen (precedence > 10) $+        showString "ConnectInfo {ciHost = " . shows (ciHost ci)+        . showString ", ciPort = " . shows (ciPort ci)+        . showString ", ciDatabase = " . shows (ciDatabase ci)+        . showString ", ciUser = " . shows (ciUser ci)+        . showString ", ciPassword = <redacted>, ciCharset = "+        . shows (ciCharset ci) . showChar '}'++-- | Local TCP defaults using full Unicode (utf8mb4). MB4 is a compatibility alias.+defaultConnectInfo, defaultConnectInfoMB4 :: ConnectInfo+defaultConnectInfo = ConnectInfo "127.0.0.1" 3306 "" "root" "" utf8mb4_unicode_ci+defaultConnectInfoMB4 = defaultConnectInfo+utf8_general_ci, utf8mb4_unicode_ci :: Word8+utf8_general_ci=33+utf8mb4_unicode_ci=224++bUFSIZE :: Int+bUFSIZE=16384++-- | Open TCP with 'defaultConnectionOptions' and establish a rendering snapshot.+connect :: ConnectInfo -> IO MySQLConn+connect = connectWithOptions defaultConnectionOptions+-- | Open TCP with explicit bounds. Constructor failure closes the transport.+connectWithOptions :: ConnectionOptions -> ConnectInfo -> IO MySQLConn+connectWithOptions opts ci = snd <$> connectDetailWithOptions opts ci+-- | Open TCP and also return the server greeting.+connectDetail :: ConnectInfo -> IO (Greeting,MySQLConn)+connectDetail = connectDetailWithOptions defaultConnectionOptions+-- | Explicit-options variant of 'connectDetail'.+connectDetailWithOptions :: ConnectionOptions -> ConnectInfo -> IO (Greeting,MySQLConn)+connectDetailWithOptions opts = connectDetailWithAuthentication opts defaultAuthenticationOptions++-- | Open TCP with explicit external authentication. PAM still requires TLS or+-- a Unix socket; TCP permits principal-pinned mutually authenticated GSSAPI.+-- The authentication settings are discarded after establishment.+connectWithAuthentication :: ConnectionOptions -> AuthenticationOptions -> ConnectInfo -> IO MySQLConn+connectWithAuthentication opts auth ci = snd <$> connectDetailWithAuthentication opts auth ci++-- | Greeting-returning variant of 'connectWithAuthentication'.+connectDetailWithAuthentication :: ConnectionOptions -> AuthenticationOptions -> ConnectInfo -> IO (Greeting,MySQLConn)+connectDetailWithAuthentication opts auth ci = withConnectDeadline opts $ do+    validateOptions opts+    bracketOnError (TCP.connectSocketTimeout (connectTimeoutMicros opts) (ciHost ci) (ciPort ci) >>= TCP.socketToConnectionAdaptive bUFSIZE) Conn.close $ \raw -> do+        (authConn,conn) <- newAuthenticatingTransportConnection opts (Conn.source raw) (Conn.send raw) (Conn.close raw)+        greet <- authenticationDiagnostic (readGreeting authConn)+        let caps=negotiatedCapabilities opts greet+        writeIORef (connectionCapabilities conn) caps+        mysqlWrite conn (encodeToPacket 1 (mkAuthForAuthentication auth opts caps ci greet))+        q <- authenticationDiagnostic (readConnPacket authConn)+        completeAuthWithOptions auth authConn greet False (ciPassword ci) q+        when (testBit caps 5) (connectionCompressionStart conn)+        role <- initializeSession conn+        rememberInitialSession conn ci role+        pure (greet,conn)++-- | Open a Unix-domain socket with the same session and ownership contracts as TCP.+connectUnixSocket :: FilePath -> ConnectInfo -> IO MySQLConn+connectUnixSocket = connectUnixSocketWithOptions defaultConnectionOptions+-- | Open a Unix-domain socket with explicit resource bounds.+connectUnixSocketWithOptions :: ConnectionOptions -> FilePath -> ConnectInfo -> IO MySQLConn+connectUnixSocketWithOptions opts path ci = snd <$> connectUnixSocketDetailWithOptions opts path ci+-- | Open a Unix-domain socket and also return the server greeting.+connectUnixSocketDetail :: FilePath -> ConnectInfo -> IO (Greeting,MySQLConn)+connectUnixSocketDetail = connectUnixSocketDetailWithOptions defaultConnectionOptions+-- | Explicit-options variant of 'connectUnixSocketDetail'.+connectUnixSocketDetailWithOptions :: ConnectionOptions -> FilePath -> ConnectInfo -> IO (Greeting,MySQLConn)+connectUnixSocketDetailWithOptions opts = connectUnixSocketDetailWithAuthentication opts defaultAuthenticationOptions++-- | Open a Unix socket with explicit PAM/GSSAPI settings. The local socket is+-- a permitted cleartext authentication transport; its filesystem access policy+-- remains the application's responsibility.+connectUnixSocketWithAuthentication :: ConnectionOptions -> AuthenticationOptions -> FilePath -> ConnectInfo -> IO MySQLConn+connectUnixSocketWithAuthentication opts auth path ci = snd <$> connectUnixSocketDetailWithAuthentication opts auth path ci++-- | Greeting-returning variant of 'connectUnixSocketWithAuthentication'.+connectUnixSocketDetailWithAuthentication :: ConnectionOptions -> AuthenticationOptions -> FilePath -> ConnectInfo -> IO (Greeting,MySQLConn)+connectUnixSocketDetailWithAuthentication opts auth path ci = withConnectDeadline opts $ do+    validateOptions opts+    let open=bracketOnError (N.socket N.AF_UNIX N.Stream 0) N.close $ \sock -> do+            N.connect sock (N.SockAddrUnix path)+            TCP.socketToConnectionAdaptive bUFSIZE (sock,N.SockAddrUnix path)+    bracketOnError open Conn.close $ \raw -> do+        (authConn,conn) <- newAuthenticatingTransportConnection opts (Conn.source raw) (Conn.send raw) (Conn.close raw)+        greet <- authenticationDiagnostic (readGreeting authConn)+        let caps=negotiatedCapabilities opts greet+        writeIORef (connectionCapabilities conn) caps+        mysqlWrite conn (encodeToPacket 1 (mkAuthForAuthentication auth opts caps ci greet))+        q <- authenticationDiagnostic (readConnPacket authConn)+        completeAuthWithOptions auth authConn greet True (ciPassword ci) q+        when (testBit caps 5) (connectionCompressionStart conn)+        role <- initializeSession conn+        rememberInitialSession conn ci role+        pure (greet,conn)++-- Greeting parsing consumes the whole envelope; an initial ERR is a server+-- failure, rather than an attempted protocol-10 decode of error text.+readGreeting :: MySQLConn -> IO Greeting+readGreeting conn = do+    packet <- readConnPacket conn+    when (isERR packet) (decodeFromPacket packet >>= throwIO . ERRException)+    getFromPacketExact getGreeting packet++negotiatedCapabilities :: ConnectionOptions -> Greeting -> Word64+-- Advertise tracking and EOF deprecation only as a pair: both OK-envelope and+-- result-terminator parsing must be available before either is used on the wire.+negotiatedCapabilities options greeting = (implemented .&. greetingCaps greeting) .|.+    (if enableSessionTracking options && greetingCaps greeting .&. 0x01800000 == 0x01800000 then 0x01800000 else 0)+  where+    implemented = clientCap .|. bit 34 .|. bit 37+        .|. (if enableProgressReporting options then bit 32 else 0)+        .|. (if enableCompression options then bit 5 else 0)+        .|. (if enableMetadataCaching options then bit 36 else 0)+        .|. (if enableExtendedMetadata options then bit 35 else 0)++-- | A required GSSAPI principal also forbids sending a password proof before+-- that method has been negotiated. MariaDB normally greets with native-password+-- even for GSSAPI accounts; send an empty initial response and require its switch.+mkAuthForAuthentication :: AuthenticationOptions -> ConnectionOptions -> Word64 -> ConnectInfo -> Greeting -> Auth+mkAuthForAuthentication auth options caps ci greeting+    | isJust (authenticationGSSAPIPrincipal auth) = Auth caps+        (fromIntegral (maximumMessageBytes options)) (ciCharset ci) (ciUser ci)+        B.empty (ciDatabase ci) (greetingAuthPlugin greeting)+    | otherwise = mkAuthWithOptions options caps ci greeting++mkAuthWithOptions :: ConnectionOptions -> Word64 -> ConnectInfo -> Greeting -> Auth+mkAuthWithOptions options caps ci greeting =+    let Auth _ _ charset user response database plugin = mkAuth (ciDatabase ci) (ciUser ci) (ciPassword ci) (ciCharset ci) greeting+    in Auth caps (fromIntegral (maximumMessageBytes options)) charset user response database plugin++mkAuth :: ByteString -> ByteString -> ByteString -> Word8 -> Greeting -> Auth+mkAuth db user pass charset greet =+    let salt = greetingSalt1 greet `B.append` greetingSalt2 greet+        plugin = greetingAuthPlugin greet+        scambleBuf = scrambleForPlugin plugin salt pass+    in Auth clientCap clientMaxPacketSize charset user scambleBuf db plugin++-- | Dispatch scramble based on the authentication plugin name.+scrambleForPlugin :: ByteString -> ByteString -> ByteString -> ByteString+scrambleForPlugin plugin salt pass+    | plugin == "caching_sha2_password" = checkedNonce 20 (scrambleSHA256 salt pass)+    | plugin == "mysql_native_password" = checkedNonce 20 (scrambleSHA1 salt pass)+    | plugin == "client_ed25519" = checkedNonce 32 (scrambleEd25519 salt pass)+    | plugin == "parsec" = checkedNonce 32 B.empty+    -- External plugins begin their conversation after the client header. No+    -- password/token may be emitted before transport and opt-in checks.+    | plugin `elem` ["dialog", "mysql_clear_password", "auth_gssapi_client"] = B.empty+    | otherwise = throw (AuthException ("unsupported MariaDB authentication plugin: " ++ show plugin))+  where+    checkedNonce count result+        | B.length salt == count = result+        | otherwise = throw (AuthException "invalid authentication challenge length")++-- | SHA1-based scramble for @mysql_native_password@.+scrambleSHA1 :: ByteString -> ByteString -> ByteString+scrambleSHA1 salt pass+    | B.null pass = B.empty+    | otherwise   = B.pack (B.zipWith xor sha1pass withSalt)+    where sha1pass = sha1 pass+          withSalt = sha1 (salt `B.append` sha1 sha1pass)+          sha1 :: ByteString -> ByteString+          sha1 = BA.convert . (Crypto.hash :: ByteString -> Crypto.Digest Crypto.SHA1)++-- | SHA256-based scramble for @caching_sha2_password@.+-- XOR(SHA256(password), SHA256(SHA256(SHA256(password)) + nonce))+scrambleSHA256 :: ByteString -> ByteString -> ByteString+scrambleSHA256 salt pass+    | B.null pass = B.empty+    | otherwise   = B.pack (B.zipWith xor sha256pass withSalt)+    where sha256pass = sha256 pass+          withSalt   = sha256 (sha256 sha256pass `B.append` salt)+          sha256 :: ByteString -> ByteString+          sha256 = BA.convert . (Crypto.hash :: ByteString -> Crypto.Digest Crypto.SHA256)++-- | MariaDB's password-based ed25519 variant. The password itself is hashed+-- once to an expanded scalar/prefix, unlike standard Ed25519's 32-byte seed.+-- Arithmetic uses crypton's constant-time Edwards primitives; no server point+-- or client-implemented curve arithmetic is involved.+scrambleEd25519 :: ByteString -> ByteString -> ByteString+scrambleEd25519 nonce password = encodedR <> Edwards.scalarEncode signed+  where+    digest = BA.convert (Crypto.hash password :: Crypto.Digest Crypto.SHA512) :: ByteString+    scalarBytes = B.pack ([B.head digest .&. 248] ++ B.unpack (B.take 30 (B.drop 1 digest))+                         ++ [(B.index digest 31 .&. 63) .|. 64])+    scalar = throwCryptoError (Edwards.scalarDecodeLong scalarBytes)+    hashScalar bytes = throwCryptoError (Edwards.scalarDecodeLong+        (BA.convert (Crypto.hash bytes :: Crypto.Digest Crypto.SHA512) :: BA.ScrubbedBytes))+    public = Edwards.pointEncode (Edwards.toPoint scalar) :: ByteString+    nonceScalar = hashScalar (B.drop 32 digest <> nonce)+    encodedR = Edwards.pointEncode (Edwards.toPoint nonceScalar)+    challenge = hashScalar (encodedR <> public <> nonce)+    signed = Edwards.scalarAdd nonceScalar (Edwards.scalarMul challenge scalar)++-- | Bound the entire connection establishment, including resolver, TLS,+-- authentication and initial session queries. The enclosing brackets release+-- any acquired transports before this timeout becomes a public exception.+withConnectDeadline :: ConnectionOptions -> IO a -> IO a+withConnectDeadline options action = do+    validateOptions options+    timeout (connectTimeoutMicros options) action >>= maybe+        (throwIO (AuthException "MariaDB connection establishment timed out")) pure++-- | Packet decoders retain failing buffers for ordinary query diagnostics.+-- Authentication parsing must not disclose them. Keep this wrapper around the+-- decoder itself so exceptions deliberately raised by application callbacks+-- retain their original type and value.+authenticationDiagnostic :: IO a -> IO a+authenticationDiagnostic action = action `catch` \(_ :: DecodePacketException) ->+    throwIO (AuthException "malformed authentication packet")++-- | Complete a bounded plugin conversation on this connection. Plugin and nonce+-- always travel together, including auth switches. Cleartext full-auth responses+-- are allowed only over TLS or a Unix-domain socket. Malformed messages and+-- unsupported plugins fail closed; the constructor owns transport cleanup.+completeAuth :: MySQLConn -> Greeting -> Bool -> ByteString -> Packet -> IO ()+completeAuth = completeAuthWithOptions defaultAuthenticationOptions++-- | External-authentication variant. Switches embed raw plugin data; later+-- tokens use AuthMoreData. Decode that envelope before invoking a plugin, and+-- accept terminal OK/ERR only in their original SQL-protocol envelope.+completeAuthWithOptions :: AuthenticationOptions -> MySQLConn -> Greeting -> Bool -> ByteString -> Packet -> IO ()+completeAuthWithOptions auth conn greeting secure password first = do+    checkPacket first+    when (isERR first) (authError first)+    if greetingAuthPlugin greeting `elem` externalPlugins+    then external 0 (greetingAuthPlugin greeting) (unwrapPlugin first)+    else loop 0 (greetingAuthPlugin greeting) initialNonce first+  where+    initialNonce = greetingSalt1 greeting <> greetingSalt2 greeting+    input = mysqlRead conn+    output = mysqlWrite conn+    send packet body = output (Packet (fromIntegral (B.length body)) (pSeqN packet + 1) (L.fromStrict body))+    next = readPacketWithLimit (authenticationWireLimit (connectionOptions conn)) input+    authError packet = authenticationDiagnostic (decodeFromPacket packet) >>= throwIO . ERRException+    authOK packet = void (authenticationDiagnostic (consumeOK conn packet))+    checkPacket packet = when (pLen packet < 0 || fromIntegral (pLen packet) > authenticationWireLimit (connectionOptions conn))+        (throwIO (AuthException "authentication packet exceeds message limit"))+    loop :: Int -> ByteString -> ByteString -> Packet -> IO ()+    loop rounds plugin nonce packet+        | rounds >= 16 = throwIO (AuthException "too many authentication exchanges")+        | isERR packet = authError packet+        -- Requiring a principal is an authentication-method requirement, not+        -- merely permission to use the credential cache if a server asks.+        -- Reject OK and password continuations before any alternate response.+        | isJust (authenticationGSSAPIPrincipal auth) && not (isAuthSwitch packet) =+            throwIO (AuthException "MariaDB did not select required GSSAPI authentication")+        | isOK packet = authOK packet+        | isAuthSwitch packet = do+            checkPacket packet+            let (name, rest) = B.break (==0) (B.drop 1 (L.toStrict (pBody packet)))+            when (B.null name || B.length name > 128 || B.null rest)+                (throwIO (AuthException "malformed authentication switch"))+            when (isJust (authenticationGSSAPIPrincipal auth) && name /= "auth_gssapi_client")+                (throwIO (AuthException "MariaDB requested another method instead of required GSSAPI"))+            let challenge = B.drop 1 rest+            if name `elem` externalPlugins+                then external (rounds+1) name+                    (Packet (fromIntegral (B.length challenge)) (pSeqN packet) (L.fromStrict challenge))+                else passwordSwitch rounds name challenge packet+        | isAuthMoreData packet && plugin == "parsec" = do+            let parameters = B.drop 1 (L.toStrict (pBody packet))+            unless (B.length parameters == 20 && B.head parameters == 80)+                (throwIO (AuthException "invalid PARSEC salt parameters"))+            let factor = fromIntegral (B.index parameters 1) :: Int+            when (factor > 20 || (1024 :: Integer) `shiftL` factor > fromIntegral (maximumAuthIterations (connectionOptions conn)))+                (throwIO (AuthException "PARSEC work exceeds configured iteration limit"))+            let keyBytes = PBKDF2.fastPBKDF2_SHA512 (PBKDF2.Parameters (1024 `shiftL` factor) 32)+                              password (B.drop 2 parameters) :: BA.ScrubbedBytes+                key = throwCryptoError (Ed25519.secretKey keyBytes)+            clientNonce <- getRandomBytes 32+            let signature = BA.convert (Ed25519.sign key (Ed25519.toPublic key) (nonce <> clientNonce))+            send packet (clientNonce <> signature)+            next >>= loop (rounds+1) "parsec-finished" nonce+        | isAuthMoreData packet && plugin == "caching_sha2_password" = do+            let dataBytes = B.drop 1 (L.toStrict (pBody packet))+            case B.unpack dataBytes of+                [3] -> next >>= terminal+                [4] -> do+                    if secure then send packet (password <> "\0")+                    else do+                        send packet (B.singleton 2)+                        keyPacket <- next+                        when (isERR keyPacket) (authError keyPacket)+                        let body = L.toStrict (pBody keyPacket)+                        unless (isAuthMoreData keyPacket && B.length body > 1 && B.length body <= 16384)+                            (throwIO (AuthException "invalid RSA authentication key envelope"))+                        doRSAAuth output (pSeqN keyPacket+1) password nonce (B.drop 1 body)+                    next >>= terminal+                _ -> throwIO (AuthException "invalid SHA-2 authentication continuation")+        | otherwise = throwIO (AuthException "unexpected authentication response")+    passwordSwitch rounds name challenge packet = do+        let expected = if name `elem` ["client_ed25519", "parsec"] then 32 else 20+            exact = if B.length challenge == expected+1 && B.last challenge == 0+                    then B.init challenge else challenge+        response <- evaluate (scrambleForPlugin name exact password)+        send packet response+        next >>= loop (rounds+1) name exact+    -- MariaDB wraps later plugin data with AuthMoreData (0x01), while data+    -- embedded in AuthSwitchRequest is already raw. Strip the wrapper exactly+    -- once at the wire boundary; never feed it to PAM or the GSS mechanism.+    unwrapPlugin packet+        | isAuthMoreData packet = packet {pLen=pLen packet-1,pBody=L.tail (pBody packet)}+        | otherwise = packet+    nextPlugin = do+        packet <- next+        when (isERR packet) (authError packet)+        unless (isAuthMoreData packet) (throwIO (AuthException "unexpected authentication response"))+        pure (unwrapPlugin packet)+    externalPlugins = ["dialog", "mysql_clear_password", "auth_gssapi_client"]+    bounded packet = do+        when (pLen packet > 1048576) (throwIO (AuthException "authentication token exceeds 1 MiB"))+        pure (L.toStrict (pBody packet))+    external :: Int -> ByteString -> Packet -> IO ()+    external rounds plugin packet+        | isJust (authenticationGSSAPIPrincipal auth) && plugin /= "auth_gssapi_client" =+            throwIO (AuthException "MariaDB did not select required GSSAPI authentication")+        | rounds >= 16 = throwIO (AuthException "too many authentication exchanges")+        | plugin == "auth_gssapi_client" = do+            announcement <- bounded packet+            let (principal,rest) = B.break (==0) announcement+                mechanism = B.drop 1 rest+            expected <- maybe (throwIO (AuthException "GSSAPI requires an expected server principal")) pure+                (authenticationGSSAPIPrincipal auth)+            unless (not (B.null expected) && B.length expected <= 1024 && not (B.elem 0 expected)+                && principal == expected && not (B.null rest)+                && mechanism `elem` ["", "\0", "Kerberos\0", "Negotiate\0"])+                (throwIO (AuthException "GSSAPI server principal or mechanism does not match configuration"))+            (GSSAPI.withGSSAPI expected (mechanism == "Negotiate\0") $ \step ->+                gssLoop step rounds packet B.empty)+                `catch` \(GSSAPI.GSSAPIError message) -> throwIO (AuthException message)+        | otherwise = do+            unless secure (throwIO (AuthException "PAM cleartext authentication requires TLS or a Unix socket"))+            callback <- maybe (throwIO (AuthException "PAM requires an explicit dialog callback")) pure+                (authenticationDialog auth)+            if plugin == "mysql_clear_password"+                then do+                    void (bounded packet)+                    -- A clear-password switch usually carries no plugin data.+                    -- Its sequence still belongs to the switch packet.+                    reply <- callback (DialogPrompt B.empty False True 0)+                    sendReply reply+                    next >>= terminal+                else do+                    -- PAM can request a client-plugin switch before its+                    -- helper has produced a prompt. A zero-byte reply+                    -- acknowledges that switch; it contains no password.+                    question <- if pLen packet == 0+                        then send packet B.empty >> nextPlugin+                        else pure packet+                    dialogLoop callback rounds 0 question+    sendReply reply = do+        when (B.length reply > 65535 || B.elem 0 reply)+            (throwIO (AuthException "PAM reply exceeds 65535 bytes or contains NUL"))+        -- The checked writer owns the next sequence, including an empty-data+        -- auth switch; using it avoids a second read before the required reply.+        FramingState sequenceN _ <- readIORef (connectionSequence conn)+        let seqN = fromIntegral sequenceN+        output (Packet (fromIntegral (B.length reply+1)) seqN (L.fromStrict (reply <> "\0")))+    dialogLoop callback rounds question packet = do+        when (rounds >= 16) (throwIO (AuthException "too many authentication exchanges"))+        -- This is raw plugin data, not an SQL envelope. In particular, an+        -- ERR-shaped token must not become a server error containing its bytes.+        body <- bounded packet+        unless (not (B.null body) && B.head body `elem` [2,3,4,5])+            (throwIO (AuthException "malformed PAM dialog question"))+        let kind = B.head body+            prompt = B.tail body+            -- auth_pam sends packet-length-delimited text without a NUL;+            -- dialog plugins may also supply one terminal NUL.+            text = if not (B.null prompt) && B.last prompt == 0 then B.init prompt else prompt+        when (B.length text > 65535 || B.elem 0 text)+            (throwIO (AuthException "PAM prompt exceeds 65535 bytes or contains NUL"))+        reply <- callback (DialogPrompt text (kind < 4) (odd kind) question)+        sendReply reply+        packet' <- next+        if odd kind || isOK packet' || isERR packet'+            then terminal packet'+            else do+                unless (isAuthMoreData packet') (throwIO (AuthException "unexpected authentication continuation"))+                dialogLoop callback (rounds+1) (question+1) (unwrapPlugin packet')+    gssLoop step rounds packet token = do+        when (rounds >= 16) (throwIO (AuthException "too many authentication exchanges"))+        (complete,response) <- step token+        unless (B.null response) (send packet response)+        reply <- next+        if complete then terminal reply else do+            when (isERR reply) (authError reply)+            -- An early SQL OK is not evidence of a completed mutual context.+            unless (isAuthMoreData reply)+                (throwIO (AuthException "GSSAPI context ended before mutual authentication"))+            inputToken <- bounded (unwrapPlugin reply)+            gssLoop step (rounds+1) reply inputToken+    terminal packet+        | isOK packet = authOK packet+        | isERR packet = authError packet+        | otherwise = throwIO (AuthException "unexpected authentication response")++-- | Full auth handler for plain TCP connections using RSA encryption.+--+-- The nonce is captured via partial application at the call site:+-- @completeAuth is writePacket pass q (plainFullAuth salt)@+plainFullAuth :: ByteString -> Word8 -> ByteString -> (Packet -> IO ()) -> InputStream Packet -> IO ()+plainFullAuth nonce seqN pass writePacket is = do+    -- Request server's RSA public key+    writePacket (putToPacket seqN (Binary.putWord8 0x02))+    keyPkt <- readPacket is+    -- Key packet body: 0x01 <PEM bytes>+    let pemKey = L.toStrict (L.drop 1 (pBody keyPkt))+    doRSAAuth writePacket (pSeqN keyPkt + 1) pass nonce pemKey+    q <- readPacket is+    if isOK q then pure ()+              else decodeFromPacket q >>= throwIO . ERRException++-- | Parse a PEM-encoded SubjectPublicKeyInfo RSA public key (PKCS#8 / RFC 5480).+parseRSAPublicKey :: ByteString -> Either String RSA.PublicKey+parseRSAPublicKey pemBytes =+    either (Left . show) Right (PEM.pemParseBS pemBytes)+    >>= firstPEM+    >>= decodeDER+    >>= spkiBitString+    >>= decodeDER+    >>= rsaKey+  where+    firstPEM []    = Left "empty PEM"+    firstPEM (x:_) = Right (PEM.pemContent x)++    decodeDER bs = either (Left . show) Right (ASN1E.decodeASN1' ASN1B.DER bs)++    spkiBitString (ASN1.Start ASN1.Sequence+                   : ASN1.Start ASN1.Sequence+                   : ASN1.OID [1,2,840,113549,1,1,1]+                   : ASN1.Null+                   : ASN1.End ASN1.Sequence+                   : ASN1.BitString bs+                   : ASN1.End ASN1.Sequence+                   : [])+        = Right (ASN1BA.bitArrayGetData bs)+    spkiBitString xs = Left ("unexpected SPKI ASN1: " ++ show xs)++    rsaKey (ASN1.Start ASN1.Sequence+            : ASN1.IntVal n+            : ASN1.IntVal e+            : ASN1.End ASN1.Sequence+            : [])+        -- Check signed ASN.1 integers before positive-integer serialization.+        -- The SPKI algorithm above must be rsaEncryption, the OAEP key type.+        | n <= 0 || e < 3 || even e || e > 4294967295 = Left "invalid RSA public integers"+        | otherwise = Right RSA.PublicKey+            { RSA.public_size = B.length (Serialize.i2osp n :: ByteString)+            , RSA.public_n    = n+            , RSA.public_e    = e+            }+    rsaKey xs = Left ("unexpected RSA key ASN1: " ++ show xs)++-- | XOR @(password ++ NUL)@ with a cyclically-extended nonce.+xorPasswordNonce :: ByteString -> ByteString -> ByteString+xorPasswordNonce pass nonce+    | B.length nonce /= 20 = throw (AuthException "invalid RSA authentication nonce length")+    | otherwise =+        let msg = pass `B.snoc` 0x00+            nonceLen = B.length nonce+            cycled = B.pack [nonce `B.index` (i `mod` nonceLen) | i <- [0 .. B.length msg - 1]]+        in B.pack (B.zipWith xor msg cycled)++-- | Encrypt password with the server's RSA public key and send it.+doRSAAuth :: (Packet -> IO ()) -> Word8 -> ByteString -> ByteString -> ByteString -> IO ()+doRSAAuth writePacket seqN pass nonce pemKey = do+    pubKey <- either (\e -> throwIO (AuthException ("RSA key parse: " ++ e))) pure+                     (parseRSAPublicKey pemKey)+    unless (RSA.public_size pubKey >= 256 && RSA.public_size pubKey <= 1024)+        (throwIO (AuthException "RSA authentication key is outside supported bounds"))+    result <- OAEP.encrypt (OAEP.defaultOAEPParams SHA1) pubKey+                           (xorPasswordNonce pass nonce)+    ct <- either (\e -> throwIO (AuthException ("RSA encrypt: " ++ show e))) pure result+    writePacket (putToPacket seqN (Binary.putByteString ct))++-- | Authentication/plugin validation or establishment-deadline failure.+-- Server ERR replies retain ERRException; cancellation keeps its original+-- exception identity. Constructors release their transport before propagating.+data AuthException = AuthException String deriving (Typeable, Show)+instance Exception AuthException+++-- Physical framing reads exactly one packet. Limits are checked before its body.+decodeInputStream :: InputStream ByteString -> IO (InputStream Packet)+decodeInputStream = decodeInputStreamWithLimit (maximumMessageBytes defaultConnectionOptions)++decodeInputStreamWithLimit :: Word64 -> InputStream ByteString -> IO (InputStream Packet)+decodeInputStreamWithLimit limit input = do+    framing <- newIORef (FramingState 256 0)+    decodeFramedInput limit framing input++decodeFramedInput :: Word64 -> IORef FramingState -> InputStream ByteString -> IO (InputStream Packet)+decodeFramedInput limit framing input = Stream.makeInputStream $ do+    header <- Stream.readExactly 4 input+    let len=fromIntegral (B.index header 0) .|. (fromIntegral (B.index header 1) `shiftL` 8) .|. (fromIntegral (B.index header 2) `shiftL` 16)+    advanceFraming limit framing len (B.index header 3)+    body <- loop [] len+    pure (Just (Packet len (B.index header 3) body))+  where+    loop acc 0 = pure (L.fromChunks (reverse acc))+    loop acc remaining = do+        chunk <- Stream.read input >>= maybe (throwIO NetworkException) pure+        if B.null chunk then loop acc remaining else do+            let count=fromIntegral (B.length chunk)+            if count >= remaining then do+                let (part,rest)=B.splitAt (fromIntegral remaining) chunk+                unless (B.null rest) (Stream.unRead rest input)+                pure (L.fromChunks (reverse (part:acc)))+            else loop (chunk:acc) (remaining-count)++readPacket :: InputStream Packet -> IO Packet+readPacket = readPacketWithLimit (maximumMessageBytes defaultConnectionOptions)++readPacketWithLimit :: Word64 -> InputStream Packet -> IO Packet+readPacketWithLimit limit input = do+    first <- Stream.read input >>= maybe (throwIO NetworkException) pure+    validateLength (pLen first)+    if pLen first < 16777215 then pure first else go (pLen first) (pSeqN first) [pBody first]+  where+    validateLength len = when (len < 0 || fromIntegral len > limit) (throwIO (ProtocolError "logical packet exceeds message limit"))+    go total sequenceN chunks = do+        packet <- Stream.read input >>= maybe (throwIO NetworkException) pure+        unless (pSeqN packet == sequenceN+1) (throwIO (ProtocolError "invalid continuation sequence"))+        when (pLen packet < 0 || pLen packet > 16777215) (throwIO (ProtocolError "invalid fragment length"))+        let size=total+pLen packet+        validateLength size+        if pLen packet < 16777215 then pure (Packet size (pSeqN packet) (L.concat (reverse (pBody packet:chunks))))+        else go size (pSeqN packet) (pBody packet:chunks)++readConnPacket :: MySQLConn -> IO Packet+readConnPacket conn+    | not (enableProgressReporting (connectionOptions conn)) = readOne+    | otherwise = reports+  where+    readOne = readPacketWithLimit (maximumMessageBytes (connectionOptions conn)) (mysqlRead conn)+    reports = do+        packet <- readOne+        if not (isProgressPacket packet) then pure packet else do+            caps <- readIORef (connectionCapabilities conn)+            unless (testBit caps 32) (throwIO (ProtocolError "unnegotiated progress packet"))+            when (pLen packet > 16384 || fromIntegral (pLen packet) > maximumMetadataBytes (connectionOptions conn))+                (throwIO (ProtocolError "progress description exceeds client limit"))+            progress <- getFromPacketExact getProgress packet+            callback <- readIORef (connectionProgressCallback conn)+            forM_ callback $ \notify -> bracket_+                (writeIORef (connectionCallbackActive conn) True)+                (writeIORef (connectionCallbackActive conn) False) (notify progress)+            reports++-- | Replace the progress callback while idle. Negotiate+-- 'enableProgressReporting' at connect time; without a callback reports are+-- consumed and discarded. Callbacks run synchronously on the command owner:+-- they must not reenter this connection. Exceptions/cancellation retire an+-- incomplete exchange. No background worker or unbounded queue exists.+setProgressCallback :: MySQLConn -> Maybe (Progress -> IO ()) -> IO ()+setProgressCallback conn callback = withCommand conn $ do+    caps <- readIORef (connectionCapabilities conn)+    unless (testBit caps 32) (throwIO (ProtocolError "progress reporting was not negotiated"))+    writeIORef (connectionProgressCallback conn) callback++writeCommand :: Command -> (Packet -> IO ()) -> IO ()+writeCommand cmd writer = let body=Binary.runPut (putCommand cmd) in writeEncoded (L.length body) body writer++writeEncoded :: Int64 -> L.ByteString -> (Packet -> IO ()) -> IO ()+writeEncoded count body writer = send count 0 body+  where+    send len sequenceN payload+        | len < 16777215 = writer (Packet len sequenceN payload)+        | otherwise = do+            let (part,rest)=L.splitAt 16777215 payload+            writer (Packet 16777215 sequenceN part)+            send (len-16777215) (sequenceN+1) rest+-- Validate/encode the complete outgoing command before marking WireInFlight.+-- Traversing bounded lazy chunks keeps oversized builders from being flattened+-- into a second contiguous allocation merely to discover their size.+sendCommand :: MySQLConn -> Command -> IO ()+sendCommand conn cmd = do+    policy <- readIORef (connectionQueryEffect conn)+    when (policy==Nothing) $ case cmd of+        COM_QUERY bytes -> classifyQueryEffect conn (Query bytes)+        COM_STMT_PREPARE bytes -> classifyQueryEffect conn (Query bytes)+        COM_STMT_EXECUTE ident _ _ -> do+            opaque <- readIORef (connectionOpaqueStatements conn)+            writeIORef (connectionQueryEffect conn) (Just (ident `elem` opaque))+        COM_STMT_EXECUTE_EXT ident _ _ _ -> do+            opaque <- readIORef (connectionOpaqueStatements conn)+            writeIORef (connectionQueryEffect conn) (Just (ident `elem` opaque))+        COM_STMT_BULK_EXECUTE ident _ _ _ -> do+            opaque <- readIORef (connectionOpaqueStatements conn)+            writeIORef (connectionQueryEffect conn) (Just (ident `elem` opaque))+        _ -> pure ()+    encoder <- case cmd of+        COM_STMT_EXECUTE ident values nullMap -> do+            let needsEncoding (MySQLText _)=True; needsEncoding _=False+            charset <- if any needsEncoding values then renderClientCharset <$> getRenderContext conn else pure "utf8mb4"+            mapM_ (either (throwIO . UnencodableParameter) pure . validateValue) values+            encoded <- forM values $ \value -> case value of+                MySQLText text -> either (throwIO . UnencodableParameter) (pure . putLenEncBytes) (encodeCharsetText charset text)+                _ -> pure (putBinaryField value)+            pure $ do+                Binary.putWord8 0x17+                Binary.putWord32le ident+                Binary.putWord8 0+                Binary.putWord32le 1+                unless (null values) $ do+                    Binary.putByteString (fromBitMap nullMap)+                    Binary.putWord8 1+                    mapM_ putParamMySQLType values+                    sequence_ encoded+        COM_STMT_EXECUTE_EXT ident flags values nullMap -> do+            unless (flags `elem` [0,1]) (throwIO (ProtocolError "unsupported cursor mode"))+            let ordinary = [value | ExecuteValue value <- values]+            charset <- if any (\value -> case value of MySQLText _ -> True; _ -> False) ordinary+                then renderClientCharset <$> getRenderContext conn else pure "utf8mb4"+            mapM_ (either (throwIO . UnencodableParameter) pure . validateValue) ordinary+            encoded <- forM values $ \value -> case value of+                ExecuteValue (MySQLText text) -> either (throwIO . UnencodableParameter)+                    (pure . putLenEncBytes) (encodeCharsetText charset text)+                ExecuteValue cell -> pure (putBinaryField cell)+                _ -> pure (pure ())+            pure $ do+                Binary.putWord8 0x17+                Binary.putWord32le ident+                Binary.putWord8 flags+                Binary.putWord32le 1+                unless (null values) $ do+                    Binary.putByteString (fromBitMap nullMap)+                    Binary.putWord8 1+                    mapM_ (putParamMySQLType . executeParameterValue) values+                    sequence_ encoded+        COM_STMT_BULK_EXECUTE ident flags types rows -> do+            caps <- readIORef (connectionCapabilities conn)+            unless (testBit caps 34 && (flags .&. 64 == 0 || testBit caps 37))+                (throwIO (ProtocolError "server did not negotiate requested bulk capability"))+            unless (flags .&. 0xff3f == 0 && flags .&. 128 /= 0)+                (throwIO (ProtocolError "invalid bulk flags"))+            let dimensionsError = ProtocolError "bulk dimensions exceed client limits or are empty"+            width <- maybe (throwIO dimensionsError) pure+                (lengthWithin (maximumColumns (connectionOptions conn)) types)+            rowCount <- maybe (throwIO dimensionsError) pure+                (lengthWithin (maximumBulkRows (connectionOptions conn)) rows)+            unless (width > 0 && rowCount > 0)+                (throwIO (ProtocolError "bulk dimensions exceed client limits or are empty"))+            ensureBulkMinimumFits conn width rowCount+            forM_ rows $ \row -> do+                unless (lengthWithin width row == Just width)+                    (throwIO (ProtocolError "bulk parameter count mismatch"))+                forM_ (zip types row) $ \(typ, cell) -> case cell of+                    BulkValue value | value /= MySQLNull -> do+                        unless (parameterTypeSignature typ == parameterTypeSignature value)+                            (throwIO (ProtocolError "bulk column has inconsistent wire types"))+                        either (throwIO . UnencodableParameter) pure (validateValue value)+                    _ -> pure ()+            let hasText = any (any (\cell -> case cell of BulkValue (MySQLText _) -> True; _ -> False)) rows+            charset <- if hasText then renderClientCharset <$> getRenderContext conn else pure "utf8mb4"+            serverLimit <- readIORef (connectionServerPacketLimit conn)+            let limit = min serverLimit (maximumMessageBytes (connectionOptions conn))+                header = 7 + 2 * fromIntegral width+            when (header > limit) (throwIO (ProtocolError "bulk header exceeds message limit"))+            remaining <- newIORef (limit - header)+            let normalize cell = do+                    left <- readIORef remaining+                    when (left == 0) (throwIO (ProtocolError "bulk exceeds message limit"))+                    normalized <- case cell of+                        BulkValue (MySQLText value) -> do+                            when (fromIntegral (T.length value) > left)+                                (throwIO (ProtocolError "bulk text exceeds message limit"))+                            either (throwIO . UnencodableParameter) (pure . BulkValue . MySQLBytes)+                                (encodeCharsetText charset value)+                        _ -> pure cell+                    let bytes = 1 + case normalized of+                            BulkValue value -> bulkValueSize value+                            _ -> 0+                    when (bytes > left) (throwIO (ProtocolError "bulk exceeds message limit"))+                    writeIORef remaining (left - bytes)+                    pure normalized+            encodedRows <- mapM (mapM normalize) rows+            pure (putCommand (COM_STMT_BULK_EXECUTE ident flags types encodedRows))+        _ -> pure (putCommand cmd)+    let body=Binary.runPut encoder+    serverLimit <- readIORef (connectionServerPacketLimit conn)+    size <- encodedLength (min serverLimit (maximumMessageBytes (connectionOptions conn))) body+    writeIORef (connectionActiveStatement conn) $ case cmd of+        COM_STMT_EXECUTE ident _ _ -> Just ident+        COM_STMT_EXECUTE_EXT ident _ _ _ -> Just ident+        COM_STMT_FETCH ident _ -> Just ident+        COM_STMT_BULK_EXECUTE ident _ _ _ -> Just ident+        _ -> Nothing+    writeIORef (connectionResultIndex conn) 0+    case cmd of+        COM_RESET_CONNECTION -> do+            -- Invalidate before writing: an interrupted reset must never leave+            -- a usable handle to a statement MariaDB may already have removed.+            atomicModifyIORef' (connectionStatements conn) (\registry ->+                (fmap (const (StatementRegistry IM.empty 0)) registry, ()))+            writeIORef (connectionOpaqueStatements conn) []+            writeIORef (connectionRenderContext conn) Nothing+            writeIORef (connectionServerStatus conn) Nothing+            invalidateSession conn+        _ -> pure ()+    setExchangeSequence conn 0+    connectionCompressionReset conn+    writeIORef (connectionExchangeProgress conn) WireInFlight+    writeEncoded size body (mysqlWrite conn)++-- Byte accounting happens while normalizing cells, before retaining another+-- batch worth of transcoded text. Fixed scalars avoid per-cell builders; only+-- bounded decimal/temporal values need their small existing encoder for sizing.+bulkValueSize :: MySQLValue -> Word64+bulkValueSize value = case value of+    MySQLNull -> 0+    MySQLBytes bytes -> stringSize bytes+    MySQLGeometry bytes -> stringSize bytes+    _ -> case parameterTypeSignature value .&. 255 of+        1 -> 1+        2 -> 2+        3 -> 4+        4 -> 4+        5 -> 8+        8 -> 8+        _ -> fromIntegral (L.length (Binary.runPut (putBinaryField value)))+  where+    stringSize bytes = let size = fromIntegral (B.length bytes)+                           prefix | size < 251 = 1+                                  | size < 65536 = 3+                                  | size < 16777216 = 4+                                  | otherwise = 9+                       in size + prefix++-- | Every cell needs an indicator even when all values are NULL/default. Reject+-- impossible batches before traversing their cells or choosing column types.+ensureBulkMinimumFits :: MySQLConn -> Int -> Int -> IO ()+ensureBulkMinimumFits conn width rows = do+    serverLimit <- readIORef (connectionServerPacketLimit conn)+    let minimumBytes = 7 + 2 * fromIntegral width + fromIntegral width * fromIntegral rows+    when (minimumBytes > min serverLimit (maximumMessageBytes (connectionOptions conn)))+        (throwIO (ProtocolError "bulk indicators exceed message limit"))++-- | Count a finite list only when it fits the bound. Inspect at most one excess+-- cons without evaluating its element. Unlike @take (limit + 1)@ this also works+-- when a caller uses 'maxBound' as a permissive resource limit.+lengthWithin :: Int -> [a] -> Maybe Int+lengthWithin limit = go 0+  where+    go !count [] = if count <= limit then Just count else Nothing+    go !count (_ : rest)+        | count < limit = go (count + 1) rest+        | otherwise = Nothing+{-# INLINE lengthWithin #-}++-- | Idempotently close the transport. Ready connections attempt bounded COM_QUIT;+-- active ones are interrupted immediately without trying to drain their reply.+close :: MySQLConn -> IO ()+close conn = mask_ $ do+    clearStatements conn+    writeIORef (connectionProgressCallback conn) Nothing+    invalidateSession conn+    prior <- atomicModifyIORef' (connectionRuntime conn) (\rt -> (rt {runtimeStatus=Closed,runtimeOwner=Nothing},runtimeStatus rt))+    unless (prior == Closed) $ do+        when (prior `elem` [Executing,Streaming,Replicating]) $+            atomicModifyIORef' (connectionFailureRef conn) (\failure ->+                (case failure of Nothing -> Just (ConnectionFailure prior False False); Just _ -> failure,()))+        writeIORef (connectionDrain conn) Nothing+        let goodbye=when (prior==Ready) $ void $ timeout (cleanupTimeoutMicros (connectionOptions conn)) $ do+                setExchangeSequence conn 0+                connectionCompressionReset conn+                writeCommand COM_QUIT (mysqlWrite conn)+        (goodbye `catch` (\(err :: SomeException) -> when (isCancellation err) (throwIO err))) `finally` releaseTransport conn++-- The cheap path recognizes flat ASCII SQL with only known stable calls.+-- Everything else uses the same lexer as parameter templates; a prefix alone+-- never establishes that an arbitrary statement cannot alter session state.+classifyQueryEffect :: MySQLConn -> Query -> IO ()+classifyQueryEffect conn qry@(Query input) = do+    context <- readIORef (connectionRenderContext conn)+    let uncertain=if simpleStableQuery input then False else+            case context >>= either (const Nothing) Just . (`compileQuery` qry) of+                Just plan -> planOpaque plan || planMultiple plan+                Nothing -> True+    writeIORef (connectionQueryEffect conn) (Just uncertain)++-- | Check server responsiveness through the ordinary command ownership guard.+ping :: MySQLConn -> IO OK+ping conn = command conn COM_PING+-- | Low-level command expecting one OK/ERR reply. Commands producing rows or no+-- reply require their dedicated APIs instead (for example 'Database.MySQL.Base.closeStmt' in Base).+command :: MySQLConn -> Command -> IO OK+command conn COM_RESET_CONNECTION = resetConnection conn+command conn cmd = withCommand conn $ sendCommand conn cmd >> readConnPacket conn >>= consumeCommandOK conn++consumeCommandOK :: MySQLConn -> Packet -> IO OK+consumeCommandOK conn packet+    | isERR packet = serverError conn packet+    | isOK packet = consumeOK conn packet+    | otherwise = throwIO (UnexpectedPacket packet)++serverError :: MySQLConn -> Packet -> IO a+serverError conn packet = do+    err <- decodeFromPacket packet+    writeIORef (connectionServerStatus conn) Nothing+    writeIORef (connectionRenderContext conn) Nothing+    invalidateSession conn+    finishResponse conn+    throwIO (ERRException err)++updateStatus :: MySQLConn -> Word16 -> IO ()+updateStatus conn flags = do+    previous <- readIORef (connectionServerStatus conn)+    unless (previous==Just flags) $ do+        writeIORef (connectionServerStatus conn) (Just flags)+        atomicModifyIORef' (connectionSession conn) (\cache -> (applySessionStatus flags cache, ()))+        when (maybe True (\old -> (old .&. 0x8200) /= (flags .&. 0x8200)) previous) $+            atomicModifyIORef' (connectionRenderContext conn) $ \ctx ->+                let change c = c {renderSQLModes=toggle "NO_BACKSLASH_ESCAPES" 0x200 (toggle "ANSI_QUOTES" 0x8000 (renderSQLModes c))}+                    toggle name flag modes = if flags .&. flag /= 0 then name:filter (/=name) modes else filter (/=name) modes+                in (fmap change ctx,())++consumeOK :: MySQLConn -> Packet -> IO OK+consumeOK conn packet = case P.parseDetailLazy getOK (pBody packet) of+    Left (buffer,offset,message) -> throwIO (DecodePacketFailed buffer offset message)+    Right (_,offset,ok) -> do+        updateStatus conn (okStatus ok)+        caps <- readIORef (connectionCapabilities conn)+        when (caps .&. 0x800000 /= 0) $ do+            when (pLen packet<offset || fromIntegral (pLen packet-offset)>maximumMetadataBytes (connectionOptions conn))+                (throwIO (ProtocolError "OK/session envelope exceeds metadata budget"))+            let trailer=L.toStrict (L.drop offset (pBody packet))+                changed=okStatus ok .&. 0x4000 /= 0+            if B.null trailer then when changed (throwIO (ProtocolError "missing session tracking payload")) else do+                (_,rest) <- either (throwIO . ProtocolError) pure (lenString trailer)+                if changed then do+                    (tracking,leftover) <- either (throwIO . ProtocolError) pure (lenString rest)+                    unless (B.null leftover) (throwIO (ProtocolError "trailing session payload"))+                    when (fromIntegral (B.length tracking)>maximumMetadataBytes (connectionOptions conn)) (throwIO (ProtocolError "session tracking payload exceeds metadata budget"))+                    changes <- either (throwIO . ProtocolError) pure (decodeSessionChanges tracking)+                    recordSessionChanges conn changes+                    mapM_ (updateTracked conn) [(name,value) | SystemVariableChanged name value <- changes]+                    -- MariaDB aliases these settings by storage offset. A+                    -- collation-only tracker cannot establish the new charset+                    -- required by the renderer; keep that context explicit.+                    when (hasVariableChange "collation_connection" changes &&+                          not (hasVariableChange "character_set_connection" changes)) $+                        writeIORef (connectionRenderContext conn) Nothing+                else unless (B.null rest) (throwIO (ProtocolError "unexpected OK trailer"))+        pure ok++-- Kept for internal wire fixtures and the rendering parser's legacy helper.+lenString :: ByteString -> Either String (ByteString,ByteString)+lenString = lengthString++trackingPairs :: ByteString -> Either String [(ByteString,ByteString)]+trackingPairs payload = do+    changes <- decodeSessionChanges payload+    pure [(name,value) | SystemVariableChanged name value <- changes]++-- | Read observed session state without claiming the wire or issuing SQL.+-- ERR, terminal failure, untracked/opaque effects and reset invalidate facts;+-- unknown values stay explicit. Last changes are a bounded latest envelope,+-- not a lossless change log. Do not infer that an absent variable is a default.+getSessionState :: MySQLConn -> IO SessionState+getSessionState conn = do+    cache <- readIORef (connectionSession conn)+    phase <- connectionStatus conn+    pure (sessionObservation (if phase `elem` [Broken,Closed]+        then emptySessionCache (sessionChangeNumber (sessionObservation cache)) else cache))++invalidateSession :: MySQLConn -> IO ()+invalidateSession conn = do+    flags <- readIORef (connectionServerStatus conn)+    atomicModifyIORef' (connectionSession conn) $ \cache ->+        let empty = emptySessionCache (sessionChangeNumber (sessionObservation cache) + 1)+        in (maybe empty (`applySessionStatus` empty) flags, ())++-- Opaque SQL may disable its own tracking. Keep the last real wire envelope+-- observable, but invalidate final-state facts rather than invent completeness.+invalidateSessionFacts :: MySQLConn -> IO ()+invalidateSessionFacts conn = do+    flags <- readIORef (connectionServerStatus conn)+    atomicModifyIORef' (connectionSession conn) $ \cache ->+        let prior = sessionObservation cache+            empty = emptySessionCache (sessionChangeNumber prior + 1)+            current = maybe empty (`applySessionStatus` empty) flags+        in (current {sessionObservation = (sessionObservation current)+                {sessionLastChanges = sessionLastChanges prior}}, ())++recordSessionChanges :: MySQLConn -> [SessionChange] -> IO ()+recordSessionChanges conn = updateSessionObservations conn TrackingEnvelope++-- Seed facts read by explicit helper SQL without fabricating tracker events.+recordSessionFacts :: MySQLConn -> [SessionChange] -> IO ()+recordSessionFacts conn = updateSessionObservations conn SnapshotFacts++updateSessionObservations :: MySQLConn -> ObservationSource -> [SessionChange] -> IO ()+updateSessionObservations conn source changes = do+    result <- atomicModifyIORef' (connectionSession conn) $ \cache ->+        case applySessionObservations source (maximumMetadataBytes (connectionOptions conn)) changes cache of+            Left message -> (cache, Left message)+            Right updated -> (updated, Right ())+    either (\message -> abortConnection conn False >> throwIO (ProtocolError message)) pure result++updateTracked :: MySQLConn -> (ByteString,ByteString) -> IO ()+updateTracked conn (name,value)+    | name `elem` ["sql_mode","character_set_client","character_set_connection","collation_connection"] && B.length value>maximumRenderingValueBytes = throwIO (ProtocolError "tracked rendering value exceeds bound")+    | name == "session_track_system_variables" = do+        let selected=BC.split ',' value+        unless (value=="*" || all (`elem` selected) ["sql_mode","character_set_client","character_set_connection"])+            (writeIORef (connectionRenderContext conn) Nothing)+    | otherwise = atomicModifyIORef' (connectionRenderContext conn) $ \ctx ->+        let apply c = case name of+                "sql_mode" -> c {renderSQLModes=BC.split ',' (B.copy value)}+                "character_set_client" -> c {renderClientCharset=B.copy value}+                "character_set_connection" -> c {renderConnectionCharset=B.copy value,renderConnectionCollation=Nothing}+                "collation_connection" -> c {renderConnectionCollation=Just (B.copy value)}+                _ -> c+        in (fmap apply ctx,())++usesOKTerminator :: MySQLConn -> IO Bool+usesOKTerminator conn = (/=0) . (.&. 0x1000000) <$> readIORef (connectionCapabilities conn)++isResultEnd :: MySQLConn -> Packet -> IO Bool+isResultEnd conn packet = (\modern -> isResultEndFor modern packet) <$> usesOKTerminator conn++isResultEndFor :: Bool -> Packet -> Bool+isResultEndFor modern packet = if modern then pLen packet > 0 && pLen packet < 16777215 && not (L.null (pBody packet)) && L.head (pBody packet)==0xfe else isEOF packet+{-# INLINE isResultEndFor #-}++consumeEnd :: MySQLConn -> Packet -> IO Word16+consumeEnd conn packet = do+    modern <- usesOKTerminator conn+    if modern then okStatus <$> consumeOK conn packet else do+        unless (pLen packet==5) (throwIO (ProtocolError "invalid legacy EOF length"))+        EOF _ flags <- decodeFromPacket packet+        updateStatus conn flags+        pure flags++readMetadataEnd :: MySQLConn -> IO ()+readMetadataEnd conn = do+    modern <- usesOKTerminator conn+    unless modern $ do+        packet <- readConnPacket conn+        unless (isEOF packet) (throwIO (UnexpectedPacket packet))+        void (consumeEnd conn packet)++-- Read a complete metadata block before publishing it into a cache. A partial+-- replacement never destroys the last fully validated decoder.+readColumnBlock :: MySQLConn -> Int -> IO CachedColumns+readColumnBlock conn count = do+    unless (count >= 0 && count <= maximumColumns (connectionOptions conn))+        (throwIO (ProtocolError "column count exceeds client limit"))+    caps <- readIORef (connectionCapabilities conn)+    let loop 0 total = pure ([], total)+        loop n total = do+            packet <- readConnPacket conn+            when (isERR packet) (serverError conn packet)+            let budget = maximumMetadataBytes (connectionOptions conn)+                size = fromIntegral (pLen packet)+            when (total > budget || size > budget - total)+                (throwIO (ProtocolError "metadata exceeds client limit"))+            field <- getFromPacketExact (getColumnMetadata (testBit caps 35)) packet+            (rest, bytes) <- loop (n - 1) (total + size)+            pure (field : rest, bytes)+    (details, bytes) <- loop count 0+    when (count > 0) (readMetadataEnd conn)+    let fields = map metadataColumn details+    pure (CachedColumns details fields count (prepareBinaryRow fields) bytes)++readColumns :: MySQLConn -> Int -> IO [ColumnDef]+readColumns conn count = cachedFields <$> readColumnBlock conn count++-- A header marker is present on text results too when caching is negotiated.+-- Omission is legal only for the exact registered statement/result ordinal.+-- Legacy metadata EOF still follows an omitted block when EOF deprecation is off.+readResultColumns :: MySQLConn -> Packet -> IO CachedColumns+readResultColumns conn packet = do+    caps <- readIORef (connectionCapabilities conn)+    (count, follows) <- getFromPacketExact (do+        n <- getLenEncInt+        flag <- if testBit caps 36 then P.getWord8 else pure 1+        unless (flag <= 1) (fail "invalid metadata-follows flag")+        pure (n, flag == 1)) packet+    unless (count > 0 && count <= maximumColumns (connectionOptions conn))+        (throwIO (ProtocolError "invalid result column count"))+    active <- readIORef (connectionActiveStatement conn)+    index <- atomicModifyIORef' (connectionResultIndex conn) (\n -> (n + 1, n))+    if follows then do+        columns <- readColumnBlock conn count+        case active of+            Just ident -> storeStatementColumns conn ident index columns+            Nothing -> pure columns+    else do+        states <- statementMap conn+        let found = active >>= (\ident -> IM.lookup (fromIntegral ident) states)+                           >>= IM.lookup index . statementResults+        case found of+            Just columns | cachedColumnCount columns == count -> do+                readMetadataEnd conn+                pure columns+            _ -> throwIO (ProtocolError "omitted metadata has no matching statement/result cache")++-- The registry and its terminal marker share one atomic reference. A close+-- racing with metadata publication can never resurrect a handle or its buffers.+statementMap :: MySQLConn -> IO (IM.IntMap StatementState)+statementMap conn = maybe IM.empty registryStatements <$> readIORef (connectionStatements conn)++-- | Registration can replace a server ID. Remove that generation's entire charge+-- atomically, so its metadata cannot remain billed to the replacement handle.+registerStatement :: MySQLConn -> StmtID -> Word64 -> Int -> IO ()+registerStatement conn ident serial arity = do+    inserted <- atomicModifyIORef' (connectionStatements conn) $ \current -> case current of+        Nothing -> (Nothing, False)+        Just registry ->+            let key = fromIntegral ident+                states = registryStatements registry+                old = maybe 0 statementMetadataBytes (IM.lookup key states)+                state = StatementState+                    { statementSerial = serial+                    , statementArity = arity+                    , statementResults = IM.empty+                    , statementMetadataBytes = 0+                    }+                updated = registry+                    { registryStatements = IM.insert key state states+                    , registryMetadataBytes = registryMetadataBytes registry - old+                    }+            in (Just updated, True)+    unless inserted (throwIO (ConnectionUnavailable Closed))++-- Cached names must own their bytes. A tiny column name decoded from a shared+-- receive chunk must not pin that chunk (and neighbouring row data) indefinitely.+-- Copy only retained statement metadata; ordinary text results keep their path.+ownColumnBlock :: CachedColumns -> IO CachedColumns+ownColumnBlock columns = do+    details <- forM (cachedDetails columns) $ \metadata -> do+        let field = metadataColumn metadata+        owned <- evaluate field+            { columnDB = B.copy (columnDB field)+            , columnTable = B.copy (columnTable field)+            , columnOrigTable = B.copy (columnOrigTable field)+            , columnName = B.copy (columnName field)+            , columnOrigName = B.copy (columnOrigName field)+            }+        attributes <- forM (metadataAttributes metadata) $ \(tag, bytes) -> do+            value <- evaluate (B.copy bytes)+            pure (tag, value)+        pure metadata {metadataColumn = owned, metadataAttributes = attributes}+    let fields = map metadataColumn details+    pure columns {cachedDetails = details, cachedFields = fields,+                  cachedBinaryDecoder = prepareBinaryRow fields}++storeStatementColumns :: MySQLConn -> StmtID -> Int -> CachedColumns -> IO CachedColumns+storeStatementColumns conn ident index columns = do+    owned <- ownColumnBlock columns+    outcome <- atomicModifyIORef' (connectionStatements conn) $ \current -> case current of+        Nothing -> (Nothing, Left (ConnectionUnavailable Closed))+        Just registry -> case IM.lookup (fromIntegral ident) (registryStatements registry) of+            Nothing -> (Just registry, Right ())+            Just state ->+                let old = maybe 0 cachedMetadataBytes (IM.lookup index (statementResults state))+                    remaining = registryMetadataBytes registry - old+                    replacement = cachedMetadataBytes columns+                    budget = maximumMetadataBytes (connectionOptions conn)+                    state' = state+                        { statementResults = IM.insert index owned (statementResults state)+                        , statementMetadataBytes = statementMetadataBytes state - old + replacement+                        }+                    registry' = registry+                        { registryStatements = IM.insert (fromIntegral ident) state' (registryStatements registry)+                        , registryMetadataBytes = remaining + replacement+                        }+                in if remaining > budget || replacement > budget - remaining+                   then (Just registry, Left (ProtocolError "aggregate statement metadata exceeds client limit"))+                   else (Just registry', Right ())+    either throwIO (const (pure owned)) outcome++forgetStatement :: MySQLConn -> StmtID -> IO ()+forgetStatement conn ident = atomicModifyIORef' (connectionStatements conn) $ \registry ->+    let remove current =+            let key = fromIntegral ident+                states = registryStatements current+                released = maybe 0 statementMetadataBytes (IM.lookup key states)+            in current+                { registryStatements = IM.delete key states+                , registryMetadataBytes = registryMetadataBytes current - released+                }+    in (fmap remove registry, ())++clearStatements :: MySQLConn -> IO ()+clearStatements conn = do+    writeIORef (connectionStatements conn) Nothing+    writeIORef (connectionOpaqueStatements conn) []++-- | Return known session facts without issuing SQL. A snapshot can become stale+-- after another operation; connected APIs obtain it under their command claim.+-- Unknown facts raise 'SessionContextUnavailable' rather than silently probing.+getRenderContext :: MySQLConn -> IO RenderContext+getRenderContext conn = readIORef (connectionRenderContext conn) >>= maybe (throwIO SessionContextUnavailable) pure++-- Startup and reset include CURRENT_ROLE in the existing snapshot exchange.+-- Ordinary public refresh keeps its six rendering facts and never redefines+-- the constructor's restoration target.+initializeSession :: MySQLConn -> IO (Maybe ByteString)+initializeSession = refreshSessionSnapshot RenderingAndRole++-- Capture the constructor's database and wire charset once. Later SET/USE+-- commands must not silently redefine what a pool's clean session means.+rememberInitialSession :: MySQLConn -> ConnectInfo -> Maybe ByteString -> IO ()+rememberInitialSession conn ci role = do+    context <- getRenderContext conn+    collation <- maybe (throwIO SessionContextUnavailable) pure (renderConnectionCollation context)+    writeIORef (connectionInitialSession conn) (Just (InitialSession+        (B.copy (ciDatabase ci)) (renderClientCharset context) collation (renderServerVersion context) role))+    recordSessionFacts conn [SchemaChanged (B.copy (ciDatabase ci))]++-- | Roll back and reset a synchronized MariaDB session without reconnecting.+-- Restores the constructor's active role, nonempty database and charset, configures required+-- tracking, and refreshes rendering facts. Other settings return to server+-- defaults; use 'resetConnectionWith' for application-specific initialization.+-- All prepared handles/raw registrations and cached decoders are invalidated.+--+-- Pending streams, transaction helpers and other command owners are rejected+-- before sending. This cannot repair a broken transport. One cleanup deadline+-- covers reset and restoration; any failure after starting retires the socket.+-- The returned OK belongs to RESET; helper SQL changes statement diagnostics.+resetConnection :: MySQLConn -> IO OK+resetConnection conn = resetConnectionWith conn (const (pure ()))++-- | Reset and initialize under one exclusive lease and cleanup deadline.+-- The callback may issue ordinary commands, but must consume all results and+-- leave no active transaction. Failure or cancellation retires the connection;+-- SQL is never replayed. A pool should open connections with an explicit+-- database: MariaDB cannot reset a selected database back to "none". A+-- constructor with no database can reset only while that fact remains known.+-- This callback runs with the caller's masking state, including buffered work.+resetConnectionWith :: MySQLConn -> (MySQLConn -> IO ()) -> IO OK+resetConnectionWith conn initialize = mask $ \restore -> do+    acquireResetLease conn+    flip finally (endTransactionLease conn) $ do+        initial <- readIORef (connectionInitialSession conn) >>= maybe+            (throwIO (ProtocolError "initial session restoration facts unavailable")) pure+        when (initialVersion initial < 100204)+            (throwIO (ProtocolError "MariaDB reset requires server 10.2.4 or later"))+        state <- getSessionState conn+        when (B.null (initialSchema initial) && sessionSchema state /= Just B.empty)+            (throwIO (ProtocolError "cannot reset a selected/unknown database to none; use an explicit constructor database"))+        let run = do+                ok <- withCommand conn $ do+                    sendCommand conn COM_RESET_CONNECTION+                    readConnPacket conn >>= consumeCommandOK conn+                -- RESET preserves the active role. Snapshot it alongside the+                -- rendering facts, then restore it before selecting a database:+                -- access to that database may come exclusively from this role.+                role <- initializeSession conn+                when (role /= initialRole initial) $ do+                    context <- getRenderContext conn+                    sql <- case initialRole initial of+                        Nothing -> pure "SET ROLE NONE"+                        Just name -> do+                            text <- either (const (throwIO (ProtocolError "invalid initial role encoding"))) pure+                                (TE.decodeUtf8' name)+                            let renderRole current = renderParams current "SET ROLE ?" [MySQLText text]+                                utf8Role = do+                                    -- A valid role can be outside the original+                                    -- client charset. Change only this rare+                                    -- restoration path, then let the ordinary+                                    -- charset restoration below put it back.+                                    void (command conn (COM_QUERY "SET NAMES utf8mb4"))+                                    refreshSessionState conn+                                    utf8 <- getRenderContext conn+                                    either throwIO pure (renderRole utf8)+                            case renderRole context of+                                Left (UnencodableParameter _) -> utf8Role+                                Left (UnsupportedRenderingContext _) -> utf8Role+                                result -> either throwIO pure result+                    void (command conn (COM_QUERY (fromQuery sql)))+                when (sessionSchema state /= Just (initialSchema initial)) $+                    void (command conn (COM_INIT_DB (initialSchema initial)))+                recordSessionFacts conn [SchemaChanged (initialSchema initial)]+                -- Without negotiated tracking, ROLE/INIT_DB invalidate the+                -- rendering snapshot. Refresh only on that restoration path;+                -- unchanged roles/databases keep the ordinary exchange count.+                restorationContext <- readIORef (connectionRenderContext conn)+                unless (isJust restorationContext) (refreshSessionState conn)+                -- MariaDB restores the authentication charset on RESET. Avoid+                -- a redundant SET NAMES unless the snapshot reports otherwise.+                context <- getRenderContext conn+                let identifier bytes = not (B.null bytes) && B.all+                        (\w -> w >= 97 && w <= 122 || w >= 65 && w <= 90 || w >= 48 && w <= 57 || w == 95) bytes+                unless (identifier (initialCharset initial) && identifier (initialCollation initial))+                    (throwIO (ProtocolError "invalid initial charset/collation identifier"))+                when (renderClientCharset context /= initialCharset initial ||+                      renderConnectionCollation context /= Just (initialCollation initial)) $ do+                    void (command conn (COM_QUERY (L.fromStrict ("SET NAMES " <> initialCharset initial+                        <> " COLLATE " <> initialCollation initial))))+                    refreshSessionState conn+                    restored <- getRenderContext conn+                    unless (renderClientCharset restored == initialCharset initial &&+                            renderConnectionCollation restored == Just (initialCollation initial))+                        (throwIO (ProtocolError "server did not restore the initial charset/collation"))+                recordSessionFacts conn [SchemaChanged (initialSchema initial)]+                initialize conn+                guardUnconsumed conn+                flags <- readIORef (connectionServerStatus conn)+                unless (maybe False (\value -> value .&. 1 == 0) flags)+                    (throwIO (ProtocolError "reset initializer left an active or unknown transaction"))+                known <- readIORef (connectionRenderContext conn)+                when (not (isJust known)) (refreshSessionState conn)+                pure ok+        result <- timeout (cleanupTimeoutMicros (connectionOptions conn))+            (restore run `onException` abortConnection conn False)+        case result of+            Just ok -> pure ok+            Nothing -> abortConnection conn False >> throwIO CleanupDeadlineExceeded++-- A reset inside withTransaction/refresh would roll back its caller's work.+-- Unlike ordinary session refresh this operation never borrows an existing+-- lease, even on the same thread. The atomic claim also excludes stream owners.+acquireResetLease :: MySQLConn -> IO ()+acquireResetLease conn = do+    tid <- myThreadId+    result <- atomicModifyIORef' (connectionRuntime conn) $ \runtime ->+        if runtimeStatus runtime == Ready && transactionOwner runtime == Nothing+        then (runtime {transactionOwner = Just tid}, Right ())+        else (runtime, Left (case runtimeStatus runtime of+            Streaming -> toException UnconsumedResultSet+            Broken -> toException (ConnectionUnavailable Broken)+            Closed -> toException (ConnectionUnavailable Closed)+            _ | transactionOwner runtime == Just tid -> toException NestedTransactionUnsupported+              | otherwise -> toException ConcurrentConnectionUse))+    either throwIO pure result++-- | Reconfigure tracking and explicitly snapshot rendering facts under one lease.+-- This executes SQL and therefore changes preceding-statement diagnostics such+-- as ROW_COUNT(). Existing tracker selections, including '*', are preserved.+refreshSessionState :: MySQLConn -> IO ()+refreshSessionState conn = void (refreshSessionSnapshot RenderingFacts conn)++-- Binary role bytes bypass character_set_results: lossy conversion could make+-- two distinct Unicode roles compare equal. The extra column costs no exchange.+-- Keep it out of ordinary refreshes, which do not establish a new clean-session role.+data SnapshotFields = RenderingFacts | RenderingAndRole deriving (Eq)++refreshSessionSnapshot :: SnapshotFields -> MySQLConn -> IO (Maybe ByteString)+refreshSessionSnapshot fieldsToRead conn = withSessionLease conn $ do+    guardUnconsumed conn+    caps <- readIORef (connectionCapabilities conn)+    when (caps .&. 0x800000 /= 0) $ void $ withCommand conn $ do+        -- This exact setup statement changes only tracker configuration. It+        -- cannot execute a routine or invalidate an already observed schema.+        writeIORef (connectionQueryEffect conn) (Just False)+        sendCommand conn (COM_QUERY "SET SESSION session_track_schema=ON, session_track_system_variables=IF(@@session.session_track_system_variables='*','*',CONCAT_WS(',',NULLIF(@@session.session_track_system_variables,''),'sql_mode,character_set_client,character_set_connection,character_set_results,session_track_system_variables,session_track_schema,session_track_transaction_info'))")+        readConnPacket conn >>= consumeCommandOK conn+    (context, limit, role) <- withCommand conn $ do+        writeIORef (connectionQueryEffect conn) (Just False)+        let query = "SELECT @@sql_mode,@@character_set_client,@@character_set_connection,@@collation_connection,VERSION(),CAST(@@max_allowed_packet AS CHAR)"+                <> if fieldsToRead == RenderingAndRole then ",CAST(CURRENT_ROLE() AS BINARY)" else ""+        sendCommand conn (COM_QUERY query)+        first <- readConnPacket conn+        when (isERR first) (serverError conn first)+        metadata <- readResultColumns conn first+        let fields = cachedFields metadata+            count = length fields+        unless (count == if fieldsToRead == RenderingAndRole then 7 else 6)+            (throwIO (ProtocolError "invalid session snapshot column count"))+        rowPacket <- readConnPacket conn+        when (fromIntegral (pLen rowPacket) > maximumMetadataBytes (connectionOptions conn))+            (throwIO (ProtocolError "session snapshot exceeds metadata budget"))+        rawValues <- getFromPacketExact (getTextRow fields) rowPacket+        -- Force/validate the snapshot while this exchange is still owned. A+        -- lazy UTF-8 exception after Ready would escape retirement, and a+        -- permissive decoder could otherwise accept an unparsed row trailer.+        let (renderingValues, roleValues) = splitAt 6 rawValues+        role <- case roleValues of+            [] -> pure Nothing+            [MySQLNull] -> pure Nothing+            [MySQLText value] -> Just <$> ownSnapshotValue (TE.encodeUtf8 value)+            [MySQLBytes value] -> do+                -- Binary results must still represent a valid role name when+                -- later rendered in the constructor's client charset.+                owned <- ownSnapshotValue value+                either (const (throwIO (ProtocolError "invalid role snapshot encoding"))) (const (pure ()))+                    (TE.decodeUtf8' owned)+                pure (Just owned)+            _ -> throwIO (ProtocolError "invalid role snapshot value type")+        values <- forM renderingValues $ \value -> do+            bytes <- case value of+                MySQLText text -> evaluate (TE.encodeUtf8 text)+                MySQLBytes raw -> pure raw+                _ -> throwIO (ProtocolError "invalid session snapshot value type")+            -- These six cold-start facts outlive the receive buffers. Retain+            -- explicitly owned, bounded storage regardless of codec internals;+            -- ordinary result payloads are not copied by this policy.+            ownSnapshotValue bytes+        snapshot <- case values of+            [modes, client, server, collation, version, packetLimit] -> do+                versionID <- maybe (throwIO (ProtocolError "invalid server version snapshot")) pure (versionNumber version)+                limit <- case BC.readInteger packetLimit of+                    Just (n, rest) | B.null rest && n > 0 && n <= fromIntegral (maxBound :: Word64) -> pure (fromIntegral n)+                    _ -> throwIO (ProtocolError "invalid server packet limit snapshot")+                pure (RenderContext (BC.split ',' modes) client server (Just collation) versionID, limit)+            _ -> throwIO (ProtocolError "invalid session snapshot")+        end <- readConnPacket conn+        isEnd <- isResultEnd conn end+        unless isEnd (throwIO (UnexpectedPacket end))+        flags <- consumeEnd conn end+        when (flags .&. 8 /= 0) (throwIO (ProtocolError "unexpected additional session snapshot result"))+        let (context, limit) = snapshot+        pure (context, limit, role)+    writeIORef (connectionServerPacketLimit conn) limit+    writeIORef (connectionRenderContext conn) (Just context)+    recordSessionFacts conn+        [SystemVariableChanged "sql_mode" (BC.intercalate "," (renderSQLModes context)),+         SystemVariableChanged "character_set_client" (renderClientCharset context),+         SystemVariableChanged "character_set_connection" (renderConnectionCharset context),+         SystemVariableChanged "collation_connection" (maybe "" id (renderConnectionCollation context))]+    pure role+  where+    -- Failure must be detected under the command claim, not turned into version+    -- zero after Ready: version gates determine which SQL bytes are executable.+    versionNumber value = case traverse component (take 3 (BC.split '.' (BC.takeWhile (\w -> w == '.' || w >= '0' && w <= '9') value))) of+        Just [a,b,c] | a > 0 -> Just (a*10000+b*100+c)+        _ -> Nothing+    component value = case BC.readInt value of+        Just (n, rest) | n >= 0 && n < 100 && B.null rest -> Just n+        _ -> Nothing+    -- Force copies while the exchange is owned; neither a role nor a rendering+    -- fact can retain a receive-buffer slice or defer work past Ready.+    ownSnapshotValue bytes = boundedSnapshotValue bytes >>= evaluate . B.copy+    boundedSnapshotValue bytes = do+        when (B.length bytes > maximumRenderingValueBytes)+            (throwIO (ProtocolError "session snapshot value exceeds bound"))+        pure bytes++guardUnconsumed :: MySQLConn -> IO ()+guardUnconsumed conn = do+    rt <- readIORef (connectionRuntime conn)+    tid <- myThreadId+    when (maybe False (/=tid) (transactionOwner rt)) (throwIO ConcurrentConnectionUse)+    case runtimeStatus rt of+        Ready -> pure ()+        Streaming -> throwIO UnconsumedResultSet+        Executing -> throwIO ConcurrentConnectionUse+        other -> throwIO (ConnectionUnavailable other)++-- Legacy low-level helpers retain their behavior for independent packet streams.+waitCommandReply :: InputStream Packet -> IO OK+waitCommandReply input = do+    packet <- readPacket input+    if isERR packet then decodeFromPacket packet >>= throwIO . ERRException+    else if isOK packet then decodeFromPacket packet else throwIO (UnexpectedPacket packet)++waitCommandReplys :: InputStream Packet -> IO [OK]+waitCommandReplys input = do+    ok <- waitCommandReply input+    if isThereMore ok then (ok:) <$> waitCommandReplys input else pure [ok]++writeIORef' :: IORef a -> a -> IO ()+writeIORef' ref value = value `seq` writeIORef ref value++-- | An expected packet/body could not be read from a closed packet source.+-- Other socket/TLS exceptions can propagate directly; inspect connection status+-- to decide whether a new connection is required.+data NetworkException = NetworkException deriving (Typeable,Show)+instance Exception NetworkException+-- | A command was attempted while the preceding result still owns the wire.+-- No bytes for the new command have been sent; consume or discard that result.+data UnconsumedResultSet = UnconsumedResultSet deriving (Typeable,Show)+instance Exception UnconsumedResultSet+-- | MariaDB returned an ERR reply. Ordinary commands consume the complete reply+-- before permitting reuse, but invalidate cached rendering facts. Replication+-- and constructor failures have their own terminal lifecycle rules.+data ERRException = ERRException ERR deriving (Typeable,Show)+instance Exception ERRException+-- | A packet marker/shape is unexpected for the current protocol phase.+data UnexpectedPacket = UnexpectedPacket Packet deriving (Typeable,Show)+instance Exception UnexpectedPacket++-- System.Timeout's private exception is not necessarily SomeAsyncException.+isCancellation :: SomeException -> Bool+isCancellation err@(SomeException inner) = isJust (fromException err :: Maybe SomeAsyncException)+    || tyConName (typeRepTyCon (typeOf inner)) == "Timeout"++-- A refresh's configuration and snapshot exchanges are one logical operation.+withSessionLease :: MySQLConn -> IO a -> IO a+withSessionLease conn action = mask $ \restore -> do+    tid <- myThreadId+    acquired <- atomicModifyIORef' (connectionRuntime conn) $ \rt -> case transactionOwner rt of+        Nothing | runtimeStatus rt==Ready -> (rt {transactionOwner=Just tid},Right True)+        Just owner | owner==tid && runtimeStatus rt==Ready -> (rt,Right False)+        _ -> (rt,Left ConcurrentConnectionUse)+    own <- either throwIO pure acquired+    restore action `finally` when own (endTransactionLease conn)++-- Force bounded encoded chunks once, without first allocating an oversized message.+encodedLength :: Word64 -> L.ByteString -> IO Int64+encodedLength limit = loop 0 . L.toChunks+  where+    loop count []=pure (fromIntegral count)+    loop count (chunk:rest)=do+        let next=count+fromIntegral (B.length chunk)+        when (next>limit) (throwIO (ProtocolError "outgoing message exceeds client/server limit"))+        next `seq` loop next rest++{-# INLINE readConnPacket #-}+{-# INLINE streamGuard #-}
+ src/Database/MySQL/Field.hs view
@@ -0,0 +1,460 @@+{-|+Module      : Database.MySQL.Field+Description : Convert between Haskell values and 'MySQLValue' fields+Copyright   : (c) Jappie Klooster, 2026+License     : BSD3+Maintainer  : Anton Antich <anton.antich@gmail.com>+Stability   : experimental+Portability : PORTABLE++A single typeclass, 'Field', for converting an individual Haskell value+to and from a 'MySQLValue'. This is the extension point libraries such as+@beam-mysql@ need to marshal user column types generically: encoding is+total ('toField'), decoding returns 'DecodeError' on the 'Left'+('fromField').++Every instance must obey the roundtrip law:++@+'fromField' ('toField' x) == 'Right' x+@++Two caveats:++* 'Maybe' wrapped around a type that itself encodes to @MySQLNull@+  (@Maybe (Maybe a)@, @Maybe MySQLValue@): SQL cannot represent a+  @Just NULL@ distinct from @NULL@, so both collapse to 'Nothing' on the+  way back. Nullable columns want exactly one 'Maybe'.++* IEEE @NaN@ fails the equation literally because @NaN /= NaN@; the+  'Float' and 'Double' encodings are still lossless and decoding returns+  the same @NaN@.++All instances delegate to named top-level functions (@encodeInt8@,+@decodeInt8@, ...) so the conversions can be used, tested and grepped+without the class.++Decoding is strict and canonical: each Haskell type decodes only from the+constructor(s) carrying exactly its payload, with no implicit numeric+widening or narrowing. There are no instances for 'Int' and 'Word'+because their width is platform dependent; use 'Data.Int.Int64' or+'Data.Word.Word64'.+-}+module Database.MySQL.Field+    ( -- * The Field class+      Field (..)+      -- * Conversion errors+    , DecodeError (..)+    , ExpectedTypeName (..)+    , EncodeStringError (..)+      -- * Named conversions backing the instances+    , encodeInt8, decodeInt8+    , encodeWord8, decodeWord8+    , encodeInt16, decodeInt16+    , encodeWord16, decodeWord16+    , encodeInt32, decodeInt32+    , encodeWord32, decodeWord32+    , encodeInt64, decodeInt64+    , encodeWord64, decodeWord64+    , encodeFloat', decodeFloat'+    , encodeDouble, decodeDouble+    , encodeScientific, decodeScientific+    , encodeText, decodeText+    , encodeLazyText, decodeLazyText+    , encodeString, encodeStringOrCrash, decodeString+    , encodeByteString, decodeByteString+    , encodeLazyByteString, decodeLazyByteString+    , encodeDay, decodeDay+    , encodeLocalTime, decodeLocalTime+    , encodeTimeOfDay, decodeTimeOfDay+    , encodeBool, decodeBool+    , encodeMaybe, decodeMaybe+    ) where++import           Control.Exception                  (Exception)+import           Data.ByteString                    (ByteString)+import qualified Data.ByteString.Lazy               as LazyByteString+import           Data.Int                           (Int16, Int32, Int64, Int8)+import           Data.Scientific                    (Scientific)+import           Data.Text                          (Text)+import qualified Data.Text                          as Text+import qualified Data.Text.Lazy                     as LazyText+import           Data.Time.Calendar                 (Day)+import           Data.Time.LocalTime                (LocalTime, TimeOfDay)+import           Data.Word                          (Word16, Word32, Word64,+                                                     Word8)+import           Database.MySQL.Protocol.MySQLValue (MySQLValue (..))++-- | The name of the Haskell type a decoder was trying to produce,+-- carried inside 'DecodeError' for error reporting.+newtype ExpectedTypeName = ExpectedTypeName Text+    deriving (Show, Eq)++-- | All the ways decoding a single field can fail.+data DecodeError+    = DecodeTypeMismatch !ExpectedTypeName !MySQLValue+      -- ^ The value's constructor does not match the requested Haskell+      -- type, e.g. asking for 'Int32' from a @MySQLText@.+    | DecodeUnexpectedNull !ExpectedTypeName+      -- ^ The value was @MySQLNull@ but the requested type is not+      -- 'Maybe'; wrap the type in 'Maybe' for nullable columns.+    deriving (Show, Eq)++instance Exception DecodeError++-- | Convert a single Haskell value to and from a 'MySQLValue'.+--+-- Instances must obey: @'fromField' ('toField' x) == 'Right' x@.+-- (Caveats: nested 'Maybe' collapses through @NULL@, and IEEE @NaN@+-- fails the literal equation; see the module header.)+class Field a where+    -- | Encode a value without SQL quoting. Most instances are total; the+    -- 'String' instance throws on lone surrogates. Use 'encodeString' when+    -- encoding untrusted 'String' input and handle its explicit error.+    toField :: a -> MySQLValue+    -- | Decode only the documented wire constructors. Nullable columns need+    -- 'Maybe'; type/NULL mismatch is returned without numeric coercion.+    fromField :: MySQLValue -> Either DecodeError a++-- | Build the error for a value whose constructor does not match,+-- mapping @MySQLNull@ to 'DecodeUnexpectedNull' and everything else to+-- 'DecodeTypeMismatch'.+decodeFailure :: ExpectedTypeName -> MySQLValue -> Either DecodeError a+decodeFailure expected MySQLNull = Left (DecodeUnexpectedNull expected)+decodeFailure expected value     = Left (DecodeTypeMismatch expected value)++-- | Construct @MySQLInt8@ without SQL rendering.+encodeInt8 :: Int8 -> MySQLValue+encodeInt8 = MySQLInt8++-- | Accept only @MySQLInt8@; NULL/type mismatch returns 'DecodeError'.+decodeInt8 :: MySQLValue -> Either DecodeError Int8+decodeInt8 (MySQLInt8 n) = Right n+decodeInt8 value         = decodeFailure (ExpectedTypeName "Int8") value++instance Field Int8 where+    toField = encodeInt8+    fromField = decodeInt8++-- | Construct @MySQLInt8U@ without SQL rendering.+encodeWord8 :: Word8 -> MySQLValue+encodeWord8 = MySQLInt8U++-- | Accept only @MySQLInt8U@; NULL/type mismatch returns 'DecodeError'.+decodeWord8 :: MySQLValue -> Either DecodeError Word8+decodeWord8 (MySQLInt8U n) = Right n+decodeWord8 value          = decodeFailure (ExpectedTypeName "Word8") value++instance Field Word8 where+    toField = encodeWord8+    fromField = decodeWord8++-- | Construct @MySQLInt16@ without SQL rendering.+encodeInt16 :: Int16 -> MySQLValue+encodeInt16 = MySQLInt16++-- | Accept only @MySQLInt16@; NULL/type mismatch returns 'DecodeError'.+decodeInt16 :: MySQLValue -> Either DecodeError Int16+decodeInt16 (MySQLInt16 n) = Right n+decodeInt16 value          = decodeFailure (ExpectedTypeName "Int16") value++instance Field Int16 where+    toField = encodeInt16+    fromField = decodeInt16++-- | Construct @MySQLInt16U@ without SQL rendering.+encodeWord16 :: Word16 -> MySQLValue+encodeWord16 = MySQLInt16U++-- | Accept only @MySQLInt16U@; NULL/type mismatch returns 'DecodeError'.+decodeWord16 :: MySQLValue -> Either DecodeError Word16+decodeWord16 (MySQLInt16U n) = Right n+decodeWord16 value           = decodeFailure (ExpectedTypeName "Word16") value++instance Field Word16 where+    toField = encodeWord16+    fromField = decodeWord16++-- | Construct @MySQLInt32@ without SQL rendering.+encodeInt32 :: Int32 -> MySQLValue+encodeInt32 = MySQLInt32++-- | Accept only @MySQLInt32@; NULL/type mismatch returns 'DecodeError'.+decodeInt32 :: MySQLValue -> Either DecodeError Int32+decodeInt32 (MySQLInt32 n) = Right n+decodeInt32 value          = decodeFailure (ExpectedTypeName "Int32") value++instance Field Int32 where+    toField = encodeInt32+    fromField = decodeInt32++-- | Construct @MySQLInt32U@ without SQL rendering.+encodeWord32 :: Word32 -> MySQLValue+encodeWord32 = MySQLInt32U++-- | Accept only @MySQLInt32U@; NULL/type mismatch returns 'DecodeError'.+decodeWord32 :: MySQLValue -> Either DecodeError Word32+decodeWord32 (MySQLInt32U n) = Right n+decodeWord32 value           = decodeFailure (ExpectedTypeName "Word32") value++instance Field Word32 where+    toField = encodeWord32+    fromField = decodeWord32++-- | Construct @MySQLInt64@ without SQL rendering.+encodeInt64 :: Int64 -> MySQLValue+encodeInt64 = MySQLInt64++-- | Accept only @MySQLInt64@; NULL/type mismatch returns 'DecodeError'.+decodeInt64 :: MySQLValue -> Either DecodeError Int64+decodeInt64 (MySQLInt64 n) = Right n+decodeInt64 value          = decodeFailure (ExpectedTypeName "Int64") value++instance Field Int64 where+    toField = encodeInt64+    fromField = decodeInt64++-- | Construct @MySQLInt64U@ without SQL rendering.+encodeWord64 :: Word64 -> MySQLValue+encodeWord64 = MySQLInt64U++-- | Accept only @MySQLInt64U@; NULL/type mismatch returns 'DecodeError'.+decodeWord64 :: MySQLValue -> Either DecodeError Word64+decodeWord64 (MySQLInt64U n) = Right n+decodeWord64 value           = decodeFailure (ExpectedTypeName "Word64") value++instance Field Word64 where+    toField = encodeWord64+    fromField = decodeWord64++-- | Named with a prime because "Prelude" already exports+-- 'Prelude.encodeFloat'.+encodeFloat' :: Float -> MySQLValue+encodeFloat' = MySQLFloat++-- | Accept only @MySQLFloat@, preserving IEEE values including NaN.+decodeFloat' :: MySQLValue -> Either DecodeError Float+decodeFloat' (MySQLFloat x) = Right x+decodeFloat' value          = decodeFailure (ExpectedTypeName "Float") value++instance Field Float where+    toField = encodeFloat'+    fromField = decodeFloat'++-- | Construct @MySQLDouble@ without SQL rendering.+encodeDouble :: Double -> MySQLValue+encodeDouble = MySQLDouble++-- | Accept only @MySQLDouble@; NULL/type mismatch returns 'DecodeError'.+decodeDouble :: MySQLValue -> Either DecodeError Double+decodeDouble (MySQLDouble x) = Right x+decodeDouble value           = decodeFailure (ExpectedTypeName "Double") value++instance Field Double where+    toField = encodeDouble+    fromField = decodeDouble++-- | Construct @MySQLDecimal@ without SQL rendering.+encodeScientific :: Scientific -> MySQLValue+encodeScientific = MySQLDecimal++-- | Accept only @MySQLDecimal@; NULL/type mismatch returns 'DecodeError'.+decodeScientific :: MySQLValue -> Either DecodeError Scientific+decodeScientific (MySQLDecimal n) = Right n+decodeScientific value            = decodeFailure (ExpectedTypeName "Scientific") value++instance Field Scientific where+    toField = encodeScientific+    fromField = decodeScientific++-- Decision: 'Text' decodes only from @MySQLText@, not from @MySQLBytes@.+-- A @MySQLBytes@ comes from a binary-collated column and need not be valid+-- UTF-8; guessing an encoding here would be a silent failure waiting to+-- happen. Read binary columns as 'ByteString' and decode explicitly.+-- | Construct @MySQLText@ without SQL rendering.+encodeText :: Text -> MySQLValue+encodeText = MySQLText++-- | Accept only @MySQLText@; NULL/type mismatch returns 'DecodeError'.+decodeText :: MySQLValue -> Either DecodeError Text+decodeText (MySQLText t) = Right t+decodeText value         = decodeFailure (ExpectedTypeName "Text") value++instance Field Text where+    toField = encodeText+    fromField = decodeText++-- | Construct @MySQLText@ without SQL rendering. Materializes the lazy input.+encodeLazyText :: LazyText.Text -> MySQLValue+encodeLazyText = MySQLText . LazyText.toStrict++-- | Accept only @MySQLText@; NULL/type mismatch returns 'DecodeError'.+decodeLazyText :: MySQLValue -> Either DecodeError LazyText.Text+decodeLazyText (MySQLText t) = Right (LazyText.fromStrict t)+decodeLazyText value         = decodeFailure (ExpectedTypeName "lazy Text") value++instance Field LazyText.Text where+    toField = encodeLazyText+    fromField = decodeLazyText++-- | All the ways encoding a 'String' can fail.+data EncodeStringError+    = EncodeStringLoneSurrogate !String+      -- ^ The 'String' contained a lone surrogate code point (@\\xD800@+      -- to @\\xDFFF@), which is not valid Unicode text and cannot be+      -- represented in MariaDB's UTF-8 wire format. @Text.pack@ would+      -- silently replace it with @U+FFFD@, corrupting the data.+    deriving (Show, Eq)++instance Exception EncodeStringError++-- | Validate Unicode scalar values and construct @MySQLText@; lone surrogates return an error.+encodeString :: String -> Either EncodeStringError MySQLValue+encodeString string =+    if any isLoneSurrogate string+        then Left (EncodeStringLoneSurrogate string)+        else Right (MySQLText (Text.pack string))++isLoneSurrogate :: Char -> Bool+isLoneSurrogate character = '\xD800' <= character && character <= '\xDFFF'++-- | Backs the 'Field' 'String' instance: the class fixes+-- @toField :: a -> MySQLValue@, so the 'EncodeStringLoneSurrogate'+-- failure from 'encodeString' crashes here instead of being returned.+encodeStringOrCrash :: String -> MySQLValue+encodeStringOrCrash string = case encodeString string of+    Right value -> value+    Left (EncodeStringLoneSurrogate offending) ->+        error ("Database.MySQL.Field.encodeString: \+               \lone surrogate code point in String: " <> show offending+               <> ". This String is not valid Unicode, so the bug is \+                  \in whatever produced it (truncated UTF-16 or bad \+                  \decoding upstream). Fix that producer, or filter \+                  \the surrogates out before calling toField.")++-- | Unpack @MySQLText@; bytes are not guessed to be UTF-8.+decodeString :: MySQLValue -> Either DecodeError String+decodeString (MySQLText t) = Right (Text.unpack t)+decodeString value         = decodeFailure (ExpectedTypeName "String") value++instance Field String where+    toField = encodeStringOrCrash+    fromField = decodeString++-- | Construct @MySQLBytes@ without SQL rendering.+encodeByteString :: ByteString -> MySQLValue+encodeByteString = MySQLBytes++-- | Accept only @MySQLBytes@; NULL/type mismatch returns 'DecodeError'.+decodeByteString :: MySQLValue -> Either DecodeError ByteString+decodeByteString (MySQLBytes bs) = Right bs+decodeByteString value           = decodeFailure (ExpectedTypeName "ByteString") value++instance Field ByteString where+    toField = encodeByteString+    fromField = decodeByteString++-- | Construct @MySQLBytes@ without SQL rendering. Materializes the lazy input.+encodeLazyByteString :: LazyByteString.ByteString -> MySQLValue+encodeLazyByteString = MySQLBytes . LazyByteString.toStrict++-- | Accept only @MySQLBytes@; NULL/type mismatch returns 'DecodeError'.+decodeLazyByteString :: MySQLValue -> Either DecodeError LazyByteString.ByteString+decodeLazyByteString (MySQLBytes bs) = Right (LazyByteString.fromStrict bs)+decodeLazyByteString value           = decodeFailure (ExpectedTypeName "lazy ByteString") value++instance Field LazyByteString.ByteString where+    toField = encodeLazyByteString+    fromField = decodeLazyByteString++-- | Construct @MySQLDate@ without SQL rendering.+encodeDay :: Day -> MySQLValue+encodeDay = MySQLDate++-- | Accept only @MySQLDate@; NULL/type mismatch returns 'DecodeError'.+decodeDay :: MySQLValue -> Either DecodeError Day+decodeDay (MySQLDate d) = Right d+decodeDay value         = decodeFailure (ExpectedTypeName "Day") value++instance Field Day where+    toField = encodeDay+    fromField = decodeDay++-- Decision: t'LocalTime' decodes from both @MySQLDateTime@ and+-- @MySQLTimeStamp@, since both carry a t'LocalTime' payload and which one+-- arrives depends on the column type, not on the Haskell type. Encoding+-- canonically produces @MySQLDateTime@; the roundtrip law still holds.+-- | Construct @MySQLDateTime@ without applying a time-zone conversion.+encodeLocalTime :: LocalTime -> MySQLValue+encodeLocalTime = MySQLDateTime++-- | Accept @MySQLDateTime@ or @MySQLTimeStamp@ in the session's time zone; raw dates fail.+decodeLocalTime :: MySQLValue -> Either DecodeError LocalTime+decodeLocalTime (MySQLDateTime t)  = Right t+decodeLocalTime (MySQLTimeStamp t) = Right t+decodeLocalTime value              = decodeFailure (ExpectedTypeName "LocalTime") value++instance Field LocalTime where+    toField = encodeLocalTime+    fromField = decodeLocalTime++-- | Construct a non-negative SQL TIME payload. The wire representation can+-- carry hours above 23; this helper does not validate a clock-time range.+-- Parameter binding subsequently checks MariaDB's range and precision.+-- For negative durations match/construct @MySQLTime@ directly.+encodeTimeOfDay :: TimeOfDay -> MySQLValue+encodeTimeOfDay = MySQLTime 0++-- | Accept a non-negative @MySQLTime@ payload; validate hours separately when a clock time is required.+decodeTimeOfDay :: MySQLValue -> Either DecodeError TimeOfDay+decodeTimeOfDay (MySQLTime sign t) =+    if sign == 0+        then Right t+        else decodeFailure (ExpectedTypeName "TimeOfDay") (MySQLTime sign t)+decodeTimeOfDay value = decodeFailure (ExpectedTypeName "TimeOfDay") value++instance Field TimeOfDay where+    toField = encodeTimeOfDay+    fromField = decodeTimeOfDay++-- Decision: 'Bool' maps to @TINYINT@, mirroring MariaDB where+-- @BOOLEAN@ is an alias for @TINYINT(1)@ and any nonzero value is true.+-- Both the signed and unsigned tiny constructors are accepted because the+-- column may be declared either way.+-- | Encode as signed @TINYINT@ zero or one, matching MariaDB's BOOLEAN alias.+encodeBool :: Bool -> MySQLValue+encodeBool True  = MySQLInt8 1+encodeBool False = MySQLInt8 0++-- | Accept signed/unsigned TINYINT; zero is False and every nonzero value is True.+decodeBool :: MySQLValue -> Either DecodeError Bool+decodeBool (MySQLInt8 n)  = Right (n /= 0)+decodeBool (MySQLInt8U n) = Right (n /= 0)+decodeBool value          = decodeFailure (ExpectedTypeName "Bool") value++instance Field Bool where+    toField = encodeBool+    fromField = decodeBool++-- | Encode Nothing as SQL NULL, otherwise delegate to 'toField'. Avoid nested nullable types.+encodeMaybe :: Field a => Maybe a -> MySQLValue+encodeMaybe Nothing      = MySQLNull+encodeMaybe (Just value) = toField value++-- | Decode NULL as Nothing; non-NULL values use the underlying 'fromField' instance.+decodeMaybe :: Field a => MySQLValue -> Either DecodeError (Maybe a)+decodeMaybe MySQLNull = Right Nothing+decodeMaybe value     = fmap Just (fromField value)++-- | @NULL@ maps to 'Nothing'; use this instance for nullable columns.+-- Do not nest it: SQL cannot distinguish @Just NULL@ from @NULL@, so+-- @Just Nothing@ decodes back as 'Nothing' and the roundtrip law breaks.+instance Field a => Field (Maybe a) where+    toField = encodeMaybe+    fromField = decodeMaybe++-- | The identity instance, an escape hatch for code that wants to handle+-- the raw protocol value in an otherwise 'Field'-polymorphic setting.+instance Field MySQLValue where+    toField = id+    fromField = Right
+ src/Database/MySQL/Protocol/Auth.hs view
@@ -0,0 +1,237 @@+{-# LANGUAGE CPP #-}+{-# OPTIONS_GHC -funbox-strict-fields #-}++{-|+Module      : Database.MySQL.Protocol.Auth+Description : Native MariaDB handshake and authentication packets+Copyright   : (c) Winterland, 2016+License     : BSD+Maintainer  : Anton Antich <anton.antich@gmail.com>+Stability   : experimental+Portability : PORTABLE++Auth related packet.++-}++module Database.MySQL.Protocol.Auth where++import           Control.Monad+import           Data.Binary+import           Data.Binary.Get+import           Data.Binary.Parser+import           Data.Binary.Put+import qualified Data.ByteString                as B+import           Data.ByteString.Char8          as BC+import           Data.Bits+import           Database.MySQL.Protocol.Packet++--------------------------------------------------------------------------------+-- Authentications++#define CLIENT_LONG_PASSWORD                  0x00000001+#define CLIENT_FOUND_ROWS                     0x00000002+#define CLIENT_LONG_FLAG                      0x00000004+#define CLIENT_CONNECT_WITH_DB                0x00000008+#define CLIENT_NO_SCHEMA                      0x00000010+#define CLIENT_COMPRESS                       0x00000020+#define CLIENT_ODBC                           0x00000040+#define CLIENT_LOCAL_FILES                    0x00000080+#define CLIENT_IGNORE_SPACE                   0x00000100+#define CLIENT_PROTOCOL_41                    0x00000200+#define CLIENT_INTERACTIVE                    0x00000400+#define CLIENT_SSL                            0x00000800+#define CLIENT_IGNORE_SIGPIPE                 0x00001000+#define CLIENT_TRANSACTIONS                   0x00002000+#define CLIENT_RESERVED                       0x00004000+#define CLIENT_SECURE_CONNECTION              0x00008000+#define CLIENT_MULTI_STATEMENTS               0x00010000+#define CLIENT_MULTI_RESULTS                  0x00020000+#define CLIENT_PS_MULTI_RESULTS               0x00040000+#define CLIENT_PLUGIN_AUTH                    0x00080000+#define CLIENT_CONNECT_ATTRS                  0x00100000+#define CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA 0x00200000++-- | Native MariaDB protocol-10 greeting. Capabilities retain both 32-bit words;+-- the salt fields are exact binary challenge bytes without a wire terminator.+data Greeting = Greeting+    { greetingProtocol :: !Word8+    , greetingVersion  :: !B.ByteString+    , greetingConnId   :: !Word32+    , greetingSalt1    :: !B.ByteString+    , greetingCaps     :: !Word64+    , greetingCharset  :: !Word8+    , greetingStatus   :: !Word16+    , greetingSalt2    :: !B.ByteString+    , greetingAuthPlugin :: !B.ByteString+    } deriving (Show, Eq)++-- | Encode the MariaDB greeting, including the upper capability word. Salt+-- fields contain exact challenge bytes, without the protocol terminator.+putGreeting :: Greeting -> Put+putGreeting (Greeting pv sv cid salt1 cap charset st salt2 plugin) = do+    putWord8 pv+    putByteString sv >> putWord8 0+    putWord32le cid+    putByteString salt1 >> putWord8 0+    putWord16le (fromIntegral cap)+    putWord8 charset+    putWord16le st+    putWord16le (fromIntegral (cap `shiftR` 16))+    putWord8 (fromIntegral (B.length salt1 + B.length salt2 + 1))+    replicateM_ 6 (putWord8 0)+    putWord32le (fromIntegral (cap `shiftR` 32))+    when (cap .&. CLIENT_SECURE_CONNECTION /= 0) (putByteString salt2 >> putWord8 0)+    when (cap .&. CLIENT_PLUGIN_AUTH /= 0) (putByteString plugin >> putWord8 0)++-- | Parse by the declared challenge length. A binary nonce may contain NUL;+-- only a plugin's known protocol terminator is removed, never an arbitrary byte.+getGreeting :: Get Greeting+getGreeting = do+    pv <- getWord8+    unless (pv == 10) (fail "unsupported MariaDB greeting protocol")+    sv <- getByteStringNul+    cid <- getWord32le+    salt1 <- getByteString 8+    skipN 1+    capL <- getWord16le+    charset <- getWord8+    status <- getWord16le+    capH <- getWord16le+    let low = fromIntegral capH `shiftL` 16 .|. fromIntegral capL+    challengeLength <- fromIntegral <$> getWord8+    skipN 6+    upper <- getWord32le+    let cap = low .|. if testBit low 0 then 0 else fromIntegral upper `shiftL` 32+    unless (cap .&. 0x88200 == 0x88200) (fail "MariaDB requires protocol-41, secure challenge and plugin authentication")+    salt2 <- if cap .&. CLIENT_SECURE_CONNECTION == 0 then pure B.empty+             else getByteString (if challengeLength >= 8 then challengeLength - 8 else 13)+    plugin <- if cap .&. CLIENT_PLUGIN_AUTH == 0 then pure "mysql_native_password"+              else getByteStringNul+    when (B.null plugin || B.length plugin > 128) (fail "invalid authentication plugin name")+    let exactLength = if plugin == "client_ed25519" || plugin == "parsec" then 32 else 20+        challenge = salt1 <> salt2+        exact = if B.length challenge == exactLength + 1 && B.last challenge == 0+                then B.init challenge else challenge+    pure (Greeting pv sv cid (B.take 8 exact) cap charset status (B.drop 8 exact) plugin)++instance Binary Greeting where+    get = getGreeting+    put = putGreeting++-- | Client response. Only implemented, server-offered capabilities belong here;+-- the upper word is emitted in reserved bytes 28..31 for a native client.+data Auth = Auth+    { authCaps      :: !Word64+    , authMaxPacket :: !Word32+    , authCharset   :: !Word8+    , authName      :: !ByteString+    , authPassword  :: !ByteString+    , authSchema    :: !ByteString+    , authPlugin    :: !ByteString+    } deriving (Show, Eq)++-- | Decode the fixed client header. The extension word occupies bytes 28..31 of both client handshake forms.+-- Legacy-marked peers use zero reserved bytes instead of an extension word.+getClientHeader :: Get (Word64, Word32, Word8)+getClientHeader = do+    low <- getWord32le+    size <- getWord32le+    charset <- getWord8+    skipN 19+    upper <- getWord32le+    pure (fromIntegral low .|. if testBit low 0 then 0 else fromIntegral upper `shiftL` 32, size, charset)++-- | Encode the fixed header shared by Auth and SSLRequest. Native extension+-- bits occupy bytes 28..31; legacy-marked packets leave that word zero.+putClientHeader :: Word64 -> Word32 -> Word8 -> Put+putClientHeader caps size charset = do+    putWord32le (fromIntegral caps)+    putWord32le size+    putWord8 charset+    replicateM_ 19 (putWord8 0)+    putWord32le (if testBit caps 0 then 0 else fromIntegral (caps `shiftR` 32))++-- | Decode this client's secure-connection response format. The password+-- response is one-byte length-delimited; database/plugin fields follow flags.+getAuth :: Get Auth+getAuth = do+    (caps, size, charset) <- getClientHeader+    name <- getByteStringNul+    len <- fromIntegral <$> getWord8+    password <- getByteString len+    schema <- if testBit caps 3 then getByteStringNul else pure B.empty+    plugin <- if testBit caps 19 then getByteStringNul else pure B.empty+    pure (Auth caps size charset name password schema plugin)++-- | Encode a response with a password payload of at most 255 bytes. The+-- connected plugin dispatcher produces bounded payloads and negotiates flags.+putAuth :: Auth -> Put+putAuth (Auth caps size charset name password schema plugin) = do+    putClientHeader caps size charset+    putByteString name >> putWord8 0+    putWord8 (fromIntegral (B.length password))+    putByteString password+    when (testBit caps 3) (putByteString schema >> putWord8 0)+    when (testBit caps 19) (putByteString plugin >> putWord8 0)++instance Binary Auth where+    get = getAuth+    put = putAuth++-- | The 32-byte pre-TLS request uses the same capability layout as Auth.+-- Its bits must agree with the following authenticated TLS response.+data SSLRequest = SSLRequest+    { sslReqCaps      :: !Word64+    , sslReqMaxPacket :: !Word32+    , sslReqCharset   :: !Word8+    } deriving (Show, Eq)++-- | Decode the complete fixed portion of the pre-TLS request. Callers+-- validating an envelope should require exact packet consumption.+getSSLRequest :: Get SSLRequest+getSSLRequest = do+    (caps, size, charset) <- getClientHeader+    pure (SSLRequest caps size charset)++-- | Encode the 32-byte request sent before starting the TLS handshake.+putSSLRequest :: SSLRequest -> Put+putSSLRequest (SSLRequest caps size charset) = putClientHeader caps size charset++instance Binary SSLRequest where+    get = getSSLRequest+    put = putSSLRequest++--------------------------------------------------------------------------------+-- default Capability Flags++-- | Implemented ordinary capabilities before intersection with the server.+-- Native extension features remain off until their readers are implemented.+clientCap :: Word64+-- Bit zero is MariaDB's legacy-client marker. Clearing it enables the+-- extension-word layout; unimplemented high-word features stay disabled.+clientCap =  CLIENT_LONG_FLAG+                .|. CLIENT_CONNECT_WITH_DB+                .|. CLIENT_IGNORE_SPACE+                .|. CLIENT_PROTOCOL_41+                .|. CLIENT_TRANSACTIONS+                .|. CLIENT_MULTI_STATEMENTS+                .|. CLIENT_MULTI_RESULTS+                .|. CLIENT_PS_MULTI_RESULTS+                .|. CLIENT_SECURE_CONNECTION+                .|. CLIENT_PLUGIN_AUTH++-- | Historical low-level default; connected APIs advertise the configured+-- logical-message limit from ConnectionOptions instead.+clientMaxPacketSize :: Word32+clientMaxPacketSize = 0x00ffffff :: Word32+++-- | Whether the greeting offers TLS, without enabling it or downgrading.+supportTLS :: Word64 -> Bool+supportTLS x = (x .&. CLIENT_SSL) /= 0++-- | Low-level default request builder. Live constructors instead intersect+-- capabilities with their server and use the configured packet limit.+sslRequest :: Word8 -> SSLRequest+sslRequest charset = SSLRequest (clientCap .|. CLIENT_SSL) clientMaxPacketSize charset
+ src/Database/MySQL/Protocol/ColumnDef.hs view
@@ -0,0 +1,210 @@+{-# LANGUAGE CPP #-}+{-# OPTIONS_GHC -funbox-strict-fields #-}++{-|+Module      : Database.MySQL.Protocol.ColumnDef+Description : MariaDB column definitions and extended metadata+Copyright   : (c) Winterland, 2016+License     : BSD+Maintainer  : Anton Antich <anton.antich@gmail.com>+Stability   : experimental+Portability : PORTABLE++Column definition(aka. field type).++-}++module Database.MySQL.Protocol.ColumnDef where++import           Data.Binary+import           Control.Monad (unless)+import           Data.Binary.Get+import           Data.Binary.Parser+import           Data.Binary.Put+import           Data.Bits                      ((.&.))+import           Data.ByteString                (ByteString)+import qualified Data.ByteString.Lazy           as L+import           Database.MySQL.Protocol.Packet++--------------------------------------------------------------------------------+--  Resultset++-- | A description of a field (column) of a table.+data ColumnDef = ColumnDef+    { -- fieldCatalog :: !ByteString              -- ^ const 'def'+      columnDB        ::  !ByteString             -- ^ Database for table.+    , columnTable     ::  !ByteString             -- ^ Table of column, if column was a field.+    , columnOrigTable ::  !ByteString             -- ^ Original table name, if table was an alias.+    , columnName      ::  !ByteString             -- ^ Name of column.+    , columnOrigName  ::  !ByteString             -- ^ Original column name, if an alias.+    , columnCharSet   ::  !Word16                 -- ^ Character set number.+    , columnLength    ::  !Word32                 -- ^ Width of column (create length).+    , columnType      ::  !FieldType+    , columnFlags     ::  !Word16                 -- ^ Div flags.+    , columnDecimals  ::  !Word8                  -- ^ Number of decimals in field.+    } deriving (Show, Eq)++-- | Decode protocol-4.1 column metadata, requiring its twelve-byte fixed+-- section. Variable-length schema/table/column names remain exact bytes.+getField :: Get ColumnDef+getField = metadataColumn <$> getColumnMetadata False+{-# INLINE getField #-}++-- | MariaDB's optional type/format attributes, preserved as exact bytes.+-- Unknown attribute tags are retained for forward compatibility. Tag 0 is the+-- native type name and tag 1 the format name. Core t'ColumnDef' stays source+-- compatible with applications that construct it positionally.+data ColumnMetadata = ColumnMetadata+    { metadataColumn :: !ColumnDef+      -- ^ Ordinary field definition shared with text and binary row decoders.+    , metadataAttributes :: ![(Word64, ByteString)]+      -- ^ Owned MariaDB extension tag/value pairs, preserving unknown tags.+    } deriving (Show, Eq)++-- | Decode the negotiated metadata layout. The extension precedes the fixed+-- twelve-byte section; accepting it as trailing bytes would shift every type.+getColumnMetadata :: Bool -> Get ColumnMetadata+getColumnMetadata extended = do+    db <- skipN 4 *> getLenEncBytes+    table <- getLenEncBytes+    originalTable <- getLenEncBytes+    name <- getLenEncBytes+    originalName <- getLenEncBytes+    extra <- if extended then getLenEncBytes >>= parseAttributes else pure []+    fixed <- getWord8+    unless (fixed == 12) (fail "invalid column fixed length")+    column <- ColumnDef db table originalTable name originalName+        <$> getWord16le <*> getWord32le <*> getFieldType+        <*> getWord16le <*> getWord8 <* skipN 2+    pure (ColumnMetadata column extra)+  where+    parseAttributes bytes = case parseDetailLazy (attributes 0 []) (L.fromStrict bytes) of+        Left (_, _, message) -> fail message+        Right (_, _, value) -> pure value+    attributes :: Int -> [(Word64, ByteString)] -> Get [(Word64, ByteString)]+    attributes count accumulated = do+        empty <- isEmpty+        if empty then pure (reverse accumulated) else do+            unless (count < 65535) (fail "too many extended metadata attributes")+            tag <- fromIntegral <$> getLenEncInt+            value <- getLenEncBytes+            attributes (count + 1) ((tag, value) : accumulated)+{-# INLINE getColumnMetadata #-}++-- | Encode column metadata, including the 0x0c fixed-section length between+-- names and charset/type information. Statement and result metadata share it.+putField :: ColumnDef -> Put+putField (ColumnDef db tbl otbl name oname charset len typ flags dec) = do+    putLenEncBytes "def"+    putLenEncBytes db+    putLenEncBytes tbl+    putLenEncBytes otbl+    putLenEncBytes name+    putLenEncBytes oname+    putWord8 0x0c+    putWord16le charset+    putWord32le len+    putFieldType typ+    putWord16le  flags+    putWord8 dec+    putWord16le 0X0000+{-# INLINE putField #-}++instance Binary ColumnDef where+    get = getField+    {-# INLINE get #-}+    put = putField+    {-# INLINE put #-}++-- | @newtype@ around 'Word8' for represent @MySQL_TYPE@, We don't use sum type here for speed reason.+--+newtype FieldType = FieldType Word8 deriving (Show, Eq)++mySQLTypeDecimal, mySQLTypeTiny, mySQLTypeShort, mySQLTypeLong, mySQLTypeFloat :: FieldType+mySQLTypeDouble, mySQLTypeNull, mySQLTypeTimestamp, mySQLTypeLongLong, mySQLTypeInt24 :: FieldType+mySQLTypeDate, mySQLTypeTime, mySQLTypeDateTime, mySQLTypeYear, mySQLTypeNewDate, mySQLTypeVarChar :: FieldType+mySQLTypeBit, mySQLTypeTimestamp2, mySQLTypeDateTime2, mySQLTypeTime2, mySQLTypeNewDecimal :: FieldType+mySQLTypeEnum, mySQLTypeSet, mySQLTypeTinyBlob, mySQLTypeMediumBlob, mySQLTypeLongBlob :: FieldType+mySQLTypeBlob, mySQLTypeVarString, mySQLTypeString, mySQLTypeGeometry :: FieldType++mySQLTypeDecimal        = FieldType 0x00+mySQLTypeTiny           = FieldType 0x01+mySQLTypeShort          = FieldType 0x02+mySQLTypeLong           = FieldType 0x03+mySQLTypeFloat          = FieldType 0x04+mySQLTypeDouble         = FieldType 0x05+mySQLTypeNull           = FieldType 0x06+mySQLTypeTimestamp      = FieldType 0x07+mySQLTypeLongLong       = FieldType 0x08+mySQLTypeInt24          = FieldType 0x09+mySQLTypeDate           = FieldType 0x0a+mySQLTypeTime           = FieldType 0x0b+mySQLTypeDateTime       = FieldType 0x0c+mySQLTypeYear           = FieldType 0x0d+mySQLTypeNewDate        = FieldType 0x0e+mySQLTypeVarChar        = FieldType 0x0f+mySQLTypeBit            = FieldType 0x10+mySQLTypeTimestamp2     = FieldType 0x11+mySQLTypeDateTime2      = FieldType 0x12+mySQLTypeTime2          = FieldType 0x13+mySQLTypeNewDecimal     = FieldType 0xf6+mySQLTypeEnum           = FieldType 0xf7+mySQLTypeSet            = FieldType 0xf8+mySQLTypeTinyBlob       = FieldType 0xf9+mySQLTypeMediumBlob     = FieldType 0xfa+mySQLTypeLongBlob       = FieldType 0xfb+mySQLTypeBlob           = FieldType 0xfc+mySQLTypeVarString      = FieldType 0xfd+mySQLTypeString         = FieldType 0xfe+mySQLTypeGeometry       = FieldType 0xff++getFieldType :: Get FieldType+getFieldType = FieldType <$> getWord8+{-# INLINE getFieldType #-}++putFieldType :: FieldType -> Put+putFieldType (FieldType t) = putWord8 t+{-# INLINE putFieldType #-}++instance Binary FieldType where+    get = getFieldType+    {-# INLINE get #-}+    put = putFieldType+    {-# INLINE put #-}++--------------------------------------------------------------------------------+--  Field flags++#define NOT_NULL_FLAG         1+#define PRI_KEY_FLAG          2+#define UNIQUE_KEY_FLAG       4+#define MULT_KEY_FLAG         8+#define BLOB_FLAG             16+#define UNSIGNED_FLAG         32+#define ZEROFILL_FLAG         64+#define BINARY_FLAG           128+#define ENUM_FLAG             256+#define AUTO_INCREMENT_FLAG   512+#define TIMESTAMP_FLAG        1024+#define SET_FLAG              2048+#define NO_DEFAULT_VALUE_FLAG 4096+#define PART_KEY_FLAG         16384+#define NUM_FLAG              32768++flagNotNull, flagPrimaryKey, flagUniqueKey, flagMultipleKey, flagBlob, flagUnsigned, flagZeroFill :: Word16 -> Bool+flagBinary, flagEnum, flagAutoIncrement, flagTimeStamp, flagSet, flagNoDefaultValue, flagPartKey, flagNumeric :: Word16 -> Bool+flagNotNull        flags = flags .&. NOT_NULL_FLAG         == NOT_NULL_FLAG+flagPrimaryKey     flags = flags .&. PRI_KEY_FLAG          == PRI_KEY_FLAG+flagUniqueKey      flags = flags .&. UNIQUE_KEY_FLAG       == UNIQUE_KEY_FLAG+flagMultipleKey    flags = flags .&. MULT_KEY_FLAG         == MULT_KEY_FLAG+flagBlob           flags = flags .&. BLOB_FLAG             == BLOB_FLAG+flagUnsigned       flags = flags .&. UNSIGNED_FLAG         == UNSIGNED_FLAG+flagZeroFill       flags = flags .&. ZEROFILL_FLAG         == ZEROFILL_FLAG+flagBinary         flags = flags .&. BINARY_FLAG           == BINARY_FLAG+flagEnum           flags = flags .&. ENUM_FLAG             == ENUM_FLAG+flagAutoIncrement  flags = flags .&. AUTO_INCREMENT_FLAG   == AUTO_INCREMENT_FLAG+flagTimeStamp      flags = flags .&. TIMESTAMP_FLAG        == TIMESTAMP_FLAG+flagSet            flags = flags .&. SET_FLAG              == SET_FLAG+flagNoDefaultValue flags = flags .&. NO_DEFAULT_VALUE_FLAG == NO_DEFAULT_VALUE_FLAG+flagPartKey        flags = flags .&. PART_KEY_FLAG         == PART_KEY_FLAG+flagNumeric        flags = flags .&. NUM_FLAG              == NUM_FLAG
+ src/Database/MySQL/Protocol/Command.hs view
@@ -0,0 +1,168 @@+{-# OPTIONS_GHC -funbox-strict-fields #-}++{-|+Module      : Database.MySQL.Protocol.Command+Description : MariaDB command encodings and bulk indicators+Copyright   : (c) Winterland, 2016+License     : BSD+Maintainer  : Anton Antich <anton.antich@gmail.com>+Stability   : experimental+Portability : PORTABLE++Common MySQL commands supports.++-}++module Database.MySQL.Protocol.Command where++import           Control.Monad+import           Data.Binary+import           Data.Binary.Get+import           Data.Binary.Parser+import           Data.Binary.Put+import           Data.ByteString                    (ByteString)+import qualified Data.ByteString.Lazy               as L+import           Database.MySQL.Protocol.MySQLValue+import           Database.MySQL.Protocol.Packet++--------------------------------------------------------------------------------+--  Commands++type StmtID = Word32++-- | One cell in a MariaDB binary bulk batch. NULL is 'BulkValue MySQLNull'.+-- DEFAULT and IGNORE are wire indicators, not SQL text; IGNORE means a default+-- on INSERT and leaves that column unchanged on UPDATE.+data BulkParameter = BulkValue !MySQLValue | BulkDefault | BulkIgnore+    deriving (Show, Eq)++-- | EXECUTE binding descriptors. Uploaded values carry a type but no inline+-- bytes. Preserve text versus binary semantics even though both upload through+-- COM_STMT_SEND_LONG_DATA; their server collations need not be the same.+data ExecuteParameter = ExecuteValue !MySQLValue | UploadedBytes | UploadedText+    deriving (Show, Eq)++-- | Type exemplar and NULL-bitmap value for an EXECUTE binding. Empty uploaded+-- parameters remain non-NULL; these exemplars are never sent as inline values.+executeParameterValue :: ExecuteParameter -> MySQLValue+executeParameterValue (ExecuteValue value) = value+executeParameterValue UploadedBytes = MySQLBytes ""+executeParameterValue UploadedText = MySQLText ""++-- | Wire commands used by MariaDB. Prefer managed operations in+-- "Database.MySQL.Base"; constructing/sending these directly can bypass arity,+-- cache-generation, ownership and negotiated-capability checks.+--+data Command+    = COM_QUIT                                    -- ^ 0x01+    | COM_INIT_DB        !ByteString              -- ^ 0x02+    | COM_QUERY          !L.ByteString            -- ^ 0x03+    | COM_PING                                    -- ^ 0x0E+    | COM_BINLOG_DUMP    !Word32 !Word16 !Word32 !ByteString -- ^ 0x12+            -- binlog-pos, flags(0x01), server-id, binlog-filename+    | COM_REGISTER_SLAVE !Word32 !ByteString !ByteString !ByteString !Word16 !Word32 !Word32 -- ^ 0x15+            -- server-id, slaves hostname, slaves user, slaves password,  slaves port, replication rank(ignored), master-id(usually 0)+    | COM_STMT_PREPARE   !L.ByteString            -- ^ 0x16 statement+    | COM_STMT_EXECUTE   !StmtID ![MySQLValue] !BitMap -- ^ 0x17 stmtId, params+    | COM_STMT_EXECUTE_EXT !StmtID !Word8 ![ExecuteParameter] !BitMap+      -- ^ 0x17: cursor flag and typed inline/uploaded parameters.+    | COM_STMT_SEND_LONG_DATA !StmtID !Word16 !ByteString+      -- ^ 0x18: statement, zero-based parameter index, one chunk; no reply.+    | COM_STMT_CLOSE     !StmtID                  -- ^ 0x19 stmtId+    | COM_STMT_RESET     !StmtID                  -- ^ 0x1A stmtId+    | COM_STMT_FETCH     !StmtID !Word32          -- ^ 0x1C: read-only cursor batch.+    | COM_RESET_CONNECTION                       -- ^ 0x1F; use the guarded reset API.+    | COM_STMT_BULK_EXECUTE !StmtID !Word16 ![MySQLValue] ![[BulkParameter]]+      -- ^ 0xFA: statement, flags, one type exemplar per column, batch rows.+    | COM_UNSUPPORTED+   deriving (Show, Eq)++putCommand :: Command -> Put+putCommand COM_QUIT              = putWord8 0x01+putCommand (COM_INIT_DB db)      = putWord8 0x02 >> putByteString db+putCommand (COM_QUERY q)         = putWord8 0x03 >> putLazyByteString q+putCommand COM_PING              = putWord8 0x0E+putCommand (COM_BINLOG_DUMP pos flags sid fname) = do+    putWord8 0x12+    putWord32le pos+    putWord16le flags+    putWord32le sid+    putByteString fname+putCommand (COM_REGISTER_SLAVE sid shost susr spass sport rrank mid) = do+    putWord8 0x15+    putWord32le sid+    putLenEncBytes shost+    putLenEncBytes susr+    putLenEncBytes spass+    putWord16le sport+    putWord32le rrank+    putWord32le mid+putCommand (COM_STMT_PREPARE stmt) = putWord8 0x16 >> putLazyByteString stmt+putCommand (COM_STMT_EXECUTE stid params nullmap) = do+    -- Preserve the ordinary encoder's direct value path: uploaded/cursor+    -- descriptors must not add a wrapper/list allocation to existing callers.+    putExecuteHeader stid 0+    unless (null params) $ do+        putByteString (fromBitMap nullmap)+        putWord8 1+        mapM_ putParamMySQLType params+        mapM_ putBinaryField params+putCommand (COM_STMT_EXECUTE_EXT stid flags params nullmap) =+    putExecute stid flags params nullmap+putCommand (COM_STMT_SEND_LONG_DATA ident parameter bytes) =+    putWord8 0x18 >> putWord32le ident >> putWord16le parameter >> putByteString bytes+putCommand (COM_STMT_FETCH ident count) = putWord8 0x1c >> putWord32le ident >> putWord32le count+putCommand (COM_STMT_CLOSE stid) = putWord8 0x19 >> putWord32le stid+putCommand (COM_STMT_RESET stid) = putWord8 0x1A >> putWord32le stid+putCommand COM_RESET_CONNECTION = putWord8 0x1f+putCommand (COM_STMT_BULK_EXECUTE ident flags types rows) = do+    putWord8 0xfa+    putWord32le ident+    putWord16le flags+    mapM_ putParamMySQLType types+    forM_ rows $ mapM_ $ \cell -> case cell of+        BulkValue MySQLNull -> putWord8 1+        BulkValue value -> putWord8 0 >> putBinaryField value+        BulkDefault -> putWord8 2+        BulkIgnore -> putWord8 3+putCommand _                     = error "unsupported command"++-- Uploaded parameters still have a type descriptor, but no inline value.+-- Their NULL bit must be clear, including when the uploaded value is empty.+putExecute :: StmtID -> Word8 -> [ExecuteParameter] -> BitMap -> Put+putExecute stid flags params nullmap = do+    putExecuteHeader stid flags+    unless (null params) $ do+        putByteString (fromBitMap nullmap)+        putWord8 0x01    -- always use new-params-bound-flag+        mapM_ (putParamMySQLType . executeParameterValue) params+        forM_ params $ \parameter -> case parameter of+            ExecuteValue value -> putBinaryField value+            _ -> pure ()++-- | Fixed EXECUTE header shared without wrapping ordinary scalar parameters.+putExecuteHeader :: StmtID -> Word8 -> Put+putExecuteHeader ident flags = putWord8 0x17 >> putWord32le ident >>+    putWord8 flags >> putWord32le 1++--------------------------------------------------------------------------------+--  Prepared statment related++-- | call 'isOK' with this packet return true+data StmtPrepareOK = StmtPrepareOK+    { stmtId        :: !StmtID+    , stmtColumnCnt :: !Int+    , stmtParamCnt  :: !Int+    , stmtWarnCnt   :: !Int+    } deriving (Show, Eq)++getStmtPrepareOK :: Get StmtPrepareOK+getStmtPrepareOK = do+    skipN 1 -- OK byte+    stmtid <- getWord32le+    cc <- fromIntegral <$> getWord16le+    pc <- fromIntegral <$> getWord16le+    skipN 1 -- reserved+    wc <- fromIntegral <$> getWord16le+    return (StmtPrepareOK stmtid cc pc wc)+{-# INLINE getStmtPrepareOK #-}
+ src/Database/MySQL/Protocol/Compression.hs view
@@ -0,0 +1,128 @@+{-# LANGUAGE BangPatterns #-}+-- | Optional MariaDB zlib framing, below ordinary packet framing and above the+-- raw TCP/TLS byte transport. Compressed frames may contain several packets or+-- just part of one. Ordinary exchanges share a modulo-256 sequence; semisync+-- replication needs a separate policy because its two directions interleave.+module Database.MySQL.Protocol.Compression+    ( CompressionTransport(..), CompressionException(..), newCompressionTransport ) where++import Control.Exception (Exception, throwIO)+import Control.Monad (unless, when)+import qualified Codec.Compression.Zlib as Zlib+import qualified Codec.Compression.Zlib.Internal as Z+import Data.Binary.Put+import Data.Bits+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as L+import Data.IORef+import Data.Word+import qualified System.IO.Streams as S++-- | Invalid compressed framing or expansion. The enclosing connection retires+-- its transport; decompression errors never become a clean end-of-stream.+data CompressionException = CompressionException String deriving Show+instance Exception CompressionException++-- | Start only after successful plugin authentication. Reset once before each+-- ordinary command, never between its continuation packets or reply packets.+data CompressionTransport = CompressionTransport+    { compressionInput :: !(S.InputStream B.ByteString)+    , compressionSend :: !(L.ByteString -> IO ())+    , compressionStart :: !(IO ())+    , compressionReset :: !(IO ())+    , compressionAllowReplicationResets :: !(IO ())+    }++-- | Construct a disabled transport. Inbound compressed and expanded frames are+-- independently bounded by the smaller of the supplied byte limit and 16 MiB-1.+-- Outbound frames contain at most 64 KiB of ordinary wire bytes. Small or+-- incompressible frames use the protocol's uncompressed representation.+newCompressionTransport :: Word64 -> Int -> S.InputStream B.ByteString+                        -> (L.ByteString -> IO ()) -> IO CompressionTransport+newCompressionTransport limit threshold input output = do+    enabled <- newIORef False+    sequenceNumber <- newIORef (0 :: Word8)+    replicationResets <- newIORef False+    pending <- newIORef []+    let bad = throwIO . CompressionException+        ceilingBytes = fromIntegral (min 16777215 (min limit (fromIntegral (maxBound :: Int))))+        advance actual = do+            expected <- readIORef sequenceNumber+            allowReset <- readIORef replicationResets+            -- MariaDB's semisync sender resets after synchronizing flushes,+            -- while ACKs travel independently in the reverse direction. As in+            -- Connector/C's pkt_nr_can_be_reset policy, no shared expected+            -- sequence exists in that dedicated stream. Ordinary commands and+            -- replication without ACKs retain the strict check.+            unless (allowReset || actual == expected) (bad "unexpected compressed packet sequence")+            writeIORef sequenceNumber (actual + 1)+        readFrame = do+            header <- S.readExactly 7 input+            let packed = word24 header 0+                expanded = word24 header 4+            when (packed == 0 || packed > ceilingBytes || expanded > ceilingBytes)+                (bad "compressed frame exceeds client limit or is empty")+            advance (B.index header 3)+            bytes <- S.readExactly packed input+            if expanded == 0 then pure [bytes]+                else L.toChunks <$> inflateExact expanded bytes+        readCompressed = do+            chunks <- readIORef pending+            case chunks of+                first:rest -> writeIORef pending rest >> pure (Just first)+                [] -> do+                    chunks' <- readFrame+                    case chunks' of+                        [] -> bad "compressed frame produced no bytes"+                        first:rest -> writeIORef pending rest >> pure (Just first)+        sendFrames bytes+            | L.null bytes = pure ()+            | otherwise = do+                let (part,rest) = L.splitAt 65536 bytes+                    count = fromIntegral (L.length part)+                    compressed = if count >= threshold then Zlib.compress part else part+                    savesBytes = count >= threshold && L.length compressed < fromIntegral count+                    payload = if savesBytes then compressed else part+                    expanded = if savesBytes then count else 0+                sequenceN <- readIORef sequenceNumber+                writeIORef sequenceNumber (sequenceN + 1)+                output (runPut (put24 (fromIntegral (L.length payload)) >> putWord8 sequenceN >>+                    put24 expanded >> putLazyByteString payload))+                sendFrames rest+    source <- S.makeInputStream $ do+        active <- readIORef enabled+        if active then readCompressed else S.read input+    let send bytes = readIORef enabled >>= \active -> if active then sendFrames bytes else output bytes+        start = writeIORef sequenceNumber 0 >> writeIORef enabled True+        reset = writeIORef sequenceNumber 0+        allowReplicationResets = writeIORef replicationResets True+    pure (CompressionTransport source send start reset allowReplicationResets)+  where+    word24 :: B.ByteString -> Int -> Int+    word24 bytes offset = fromIntegral (B.index bytes offset) .|.+        (fromIntegral (B.index bytes (offset+1)) `shiftL` 8) .|.+        (fromIntegral (B.index bytes (offset+2)) `shiftL` 16)+    put24 :: Int -> Put+    put24 n = putWord8 (fromIntegral n) >> putWord8 (fromIntegral (n `shiftR` 8)) >>+        putWord8 (fromIntegral (n `shiftR` 16))++-- Streaming zlib inspection bounds output before retaining it and checks both+-- the advertised length and compressed stream termination. The lazy pure+-- decompressor alone cannot distinguish a false length or trailing stream.+inflateExact :: Int -> B.ByteString -> IO L.ByteString+inflateExact expected input = go 0 [] [input] (Z.decompressIO Z.zlibFormat Z.defaultDecompressParams)+  where+    bad = throwIO . CompressionException+    go !size chunks pending stream = case stream of+        Z.DecompressInputRequired next -> case pending of+            [] -> next B.empty >>= go size chunks []+            first:rest -> next first >>= go size chunks rest+        Z.DecompressOutputAvailable chunk next -> do+            let total = size + B.length chunk+            when (total > expected) (bad "zlib output exceeds advertised frame length")+            next >>= go total (chunk:chunks) pending+        Z.DecompressStreamEnd trailing -> do+            unless (size == expected && B.null trailing && all B.null pending)+                (bad "zlib frame length or trailing data mismatch")+            pure (L.fromChunks (reverse chunks))+        Z.DecompressStreamError err -> bad ("invalid zlib frame: " ++ show err)
+ src/Database/MySQL/Protocol/Escape.hs view
@@ -0,0 +1,135 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE CPP #-}++{-|+Module      : Database.MySQL.Protocol.Escape+Description : Low-level SQL escaping; prefer session-aware public APIs+Copyright   : (c) Winterland, 2016+License     : BSD+Maintainer  : Anton Antich <anton.antich@gmail.com>+Stability   : experimental+Portability : PORTABLE++Low-level backslash escaping for byte/text literals. These helpers do not+observe the connection's SQL mode or charset, and are insufficient when+@NO_BACKSLASH_ESCAPES@ is enabled. Application code should use managed prepared+bindings or session-aware text parameters from "Database.MySQL.Base".++Pattern wildcards @%@ and @_@ are intentionally left unchanged: SQL literal+quoting and LIKE-pattern escaping are separate application decisions.++-}++module Database.MySQL.Protocol.Escape where++import           Control.Monad            (forM_)+import           Data.ByteString          (ByteString)+import qualified Data.ByteString.Internal as B+import           Data.Text                (Text)+import qualified Data.Text.Array          as TA+import qualified Data.Text.Internal       as T+import           Data.Word+import           Foreign.ForeignPtr       (withForeignPtr)+import           Foreign.Ptr              (Ptr, minusPtr, plusPtr)+import           Foreign.Storable         (peek, poke, pokeByteOff)+import           GHC.IO                   (unsafeDupablePerformIO)++escapeText :: Text -> Text+#if MIN_VERSION_text(2,0,0)+escapeText (T.Text arr off len)+    | len <= 0  = T.empty+    | otherwise =+        let (arr', len') =  TA.run2 $ do+                marr <- TA.new (len * 2)+                loop arr (off + len) marr off 0+        in T.Text arr' 0 len'+  where+    escape c marr ix = do+        TA.unsafeWrite marr ix 92+        TA.unsafeWrite marr (ix+1) c++    loop oarr oend marr !ix !ix'+        | ix == oend = return (marr, ix')+        | otherwise  = do+            let c = TA.unsafeIndex oarr ix+                cs = c : [ TA.unsafeIndex oarr (ix+1) | c >= 0xC0 ]+                      ++ [ TA.unsafeIndex oarr (ix+2) | c >= 0xE0 ]+                      ++ [ TA.unsafeIndex oarr (ix+3) | c >= 0xF0 ]+                go2 = loop oarr oend marr (ix+1) (ix'+2)+                goN = do+                  forM_ (zip [0..4] cs) $ \(di,c') -> TA.unsafeWrite marr (ix' + di) c'+                  loop oarr oend marr (ix + length cs) (ix' + length cs)+            if  | c == 0+                    || c == 39+                    || c == 34 -> escape c   marr ix' >> go2 -- \0 \' \"+                | c == 8       -> escape 98  marr ix' >> go2 -- \b+                | c == 10      -> escape 110 marr ix' >> go2 -- \n+                | c == 13      -> escape 114 marr ix' >> go2 -- \r+                | c == 9       -> escape 116 marr ix' >> go2 -- \t+                | c == 26      -> escape 90  marr ix' >> go2 -- \Z+                | c == 92      -> escape 92  marr ix' >> go2 -- \\++                | otherwise    -> goN+#else+escapeText (T.Text arr off len)+    | len <= 0  = T.empty+    | otherwise =+        let (arr', len') =  TA.run2 $ do+                marr <- TA.new (len * 2)+                loop arr (off + len) marr off 0+        in T.Text arr' 0 len'+  where+    escape c marr ix = do+        TA.unsafeWrite marr ix 92+        TA.unsafeWrite marr (ix+1) c++    loop oarr oend marr !ix !ix'+        | ix == oend = return (marr, ix')+        | otherwise  = do+            let c = TA.unsafeIndex oarr ix+                go1 = loop oarr oend marr (ix+1) (ix'+1)+                go2 = loop oarr oend marr (ix+1) (ix'+2)+            if  | c >= 0xD800 && c <= 0xDBFF  -> do let c2 = TA.unsafeIndex oarr (ix+1)+                                                    TA.unsafeWrite marr ix' c+                                                    TA.unsafeWrite marr (ix'+1) c2+                                                    loop oarr oend marr (ix+2) (ix'+2)+                | c == 0+                    || c == 39+                    || c == 34 -> escape c   marr ix' >> go2 -- \0 \' \"+                | c == 8       -> escape 98  marr ix' >> go2 -- \b+                | c == 10      -> escape 110 marr ix' >> go2 -- \n+                | c == 13      -> escape 114 marr ix' >> go2 -- \r+                | c == 9       -> escape 116 marr ix' >> go2 -- \t+                | c == 26      -> escape 90  marr ix' >> go2 -- \Z+                | c == 92      -> escape 92  marr ix' >> go2 -- \\++                | otherwise    -> TA.unsafeWrite marr ix' c >> go1+#endif++escapeBytes :: ByteString -> ByteString+escapeBytes (B.PS fp s len) = unsafeDupablePerformIO $ withForeignPtr fp $ \ a ->+    B.createUptoN (len * 2) $ \ b -> do+        b' <- loop (a `plusPtr` s) (a `plusPtr` s `plusPtr` len) b+        return (b' `minusPtr` b)+  where+    escape :: Word8 -> Ptr Word8 -> IO (Ptr Word8)+    escape c p = do+        poke p 92+        pokeByteOff p 1 c+        return (p `plusPtr` 2)++    loop !a aend !b+        | a == aend = return b+        | otherwise = do+            c <- peek a+            if  | c == 0+                    || c == 39+                    || c == 34 -> escape c   b >>= loop (a `plusPtr` 1) aend -- \0 \' \"+                | c == 8       -> escape 98  b >>= loop (a `plusPtr` 1) aend -- \b+                | c == 10      -> escape 110 b >>= loop (a `plusPtr` 1) aend -- \n+                | c == 13      -> escape 114 b >>= loop (a `plusPtr` 1) aend -- \r+                | c == 9       -> escape 116 b >>= loop (a `plusPtr` 1) aend -- \t+                | c == 26      -> escape 90  b >>= loop (a `plusPtr` 1) aend -- \Z+                | c == 92      -> escape 92  b >>= loop (a `plusPtr` 1) aend -- \\++                | otherwise    -> poke b c >> loop (a `plusPtr` 1) aend (b `plusPtr` 1)
+ src/Database/MySQL/Protocol/MySQLValue.hs view
@@ -0,0 +1,808 @@+{-# LANGUAGE BangPatterns #-}+{-# OPTIONS_GHC -funbox-strict-fields #-}++{-|+Module      : Database.MySQL.Protocol.MySQLValue+Description : MariaDB values and text/binary row codecs+Copyright   : (c) Winterland, 2016+License     : BSD+Maintainer  : Anton Antich <anton.antich@gmail.com>+Stability   : experimental+Portability : PORTABLE++Core text and binary row decoder/encoder machinery.++-}++module Database.MySQL.Protocol.MySQLValue+  ( -- * MySQLValue decoder and encoder+    MySQLValue(..)+  , DateParts(..)+  , decodeColumnText+  , encodeCharsetText+  , validateValue+  , parameterTypeSignature+  , putParamMySQLType+  , getTextField+  , putTextField+  , getTextRow+  , prepareTextRow+  , prepareBinaryRow+  , prepareTextRowVector+  , prepareBinaryRowVector+  , getTextRowVector+  , getBinaryField+  , putBinaryField+  , getBinaryRow+  , getBinaryRowVector+  -- * Internal utilities+  , getBits+  , BitMap(..)+  , isColumnSet+  , isColumnNull+  , makeNullMap+  ) where++import qualified Blaze.Text                         as Textual+import Control.Exception (throw)+import           Control.Monad+import           Data.Binary.Put+import           Data.Binary.Parser+import           Data.Bits+import           Data.ByteString                    (ByteString)+import qualified Data.ByteString                    as B+import qualified Data.ByteString.Builder            as BB+import           Data.ByteString.Builder.Scientific (FPFormat (..),+                                                     formatScientificBuilder)+import qualified Data.ByteString.Char8              as BC+import qualified Data.ByteString.Lazy               as L+import qualified Data.ByteString.Lex.Fractional     as LexFrac+import qualified Data.ByteString.Lex.Integral       as LexInt+import qualified Data.ByteString.Unsafe             as B+import           Data.Char (chr, ord)+import qualified Data.Text as Text+import           Data.Fixed                         (Pico)+import           Data.Int+import           Data.Scientific                    (Scientific, coefficient, base10Exponent, normalize)+import           Data.Text                          (Text)+import qualified Data.Text.Encoding                 as T+import           Data.Time.Calendar                 (Day, fromGregorianValid,+                                                     toGregorian)+import           Data.Time.Format                   (defaultTimeLocale,+                                                     formatTime)+import           Data.Time.LocalTime                (LocalTime (..),+                                                     TimeOfDay (..))+import           Data.Word+import           Database.MySQL.Protocol.ColumnDef+import           Database.MySQL.Protocol.Escape+import           Database.MySQL.Protocol.Packet+import           GHC.Generics                       (Generic)+import qualified Data.Vector                        as V++--------------------------------------------------------------------------------+-- | Unnormalized MariaDB year, month and day components. Zero and invalid+-- calendar combinations are preserved when the server's SQL mode permits them.+-- Binding checks component bounds without coercing them to a different date.+data DateParts = DateParts !Integer !Int !Int deriving (Show, Eq, Generic)++-- | Values shared by text and prepared-binary protocols. DATE and DATETIME are+-- calendar values; TIMESTAMP is converted by MariaDB through the session time+-- zone, so its returned t'LocalTime' describes that zone rather than UTC.+--+-- TIME can be a duration up to 838:59:59.999999. Its t'TimeOfDay' may therefore+-- have hours greater than 23. Built-in binding rejects sub-microsecond precision+-- rather than silently truncating it. Raw date constructors preserve values+-- that 'Day' cannot represent; ordinary valid values use the existing forms.+data MySQLValue+    = MySQLDecimal       !Scientific   -- ^ DECIMAL, NEWDECIMAL+    | MySQLInt8U         !Word8        -- ^ Unsigned TINY+    | MySQLInt8          !Int8         -- ^ TINY+    | MySQLInt16U        !Word16       -- ^ Unsigned SHORT+    | MySQLInt16         !Int16        -- ^ SHORT+    | MySQLInt32U        !Word32       -- ^ Unsigned LONG, INT24+    | MySQLInt32         !Int32        -- ^ LONG, INT24+    | MySQLInt64U        !Word64       -- ^ Unsigned LONGLONG+    | MySQLInt64         !Int64        -- ^ LONGLONG+    | MySQLFloat         !Float        -- ^ IEEE 754 single precision format+    | MySQLDouble        !Double       -- ^ IEEE 754 double precision format+    | MySQLYear          !Word16       -- ^ YEAR+    | MySQLDateTime      !LocalTime    -- ^ DATETIME+    | MySQLTimeStamp     !LocalTime    -- ^ TIMESTAMP+    | MySQLDate          !Day              -- ^ DATE+    | MySQLDateRaw       !DateParts -- ^ Zero, partial-zero or invalid calendar DATE.+    | MySQLDateTimeRaw   !DateParts !TimeOfDay -- ^ Unrepresentable DATETIME date part.+    | MySQLTimeStampRaw  !DateParts !TimeOfDay -- ^ Unrepresentable TIMESTAMP date part.+    | MySQLTime          !Word8 !TimeOfDay -- ^ sign(0 = non-negative, 1 = negative) hh mm ss microsecond+                                           -- The sign is OPPOSITE to binlog one !!!+    | MySQLGeometry      !ByteString       -- ^ Opaque geometry bytes; use MariaDB SQL conversion functions when needed.+    | MySQLBytes         !ByteString+    | MySQLBit           !Word64+    | MySQLText          !Text+    | MySQLNull+  deriving (Show, Eq, Generic)++-- | Put t'FieldType' and usigned bit(0x80/0x00) for 'MySQLValue's.+--+-- | The two-byte prepared parameter type: low byte is the wire type, high+-- byte carries unsignedness. Bulk validation compares this scalar without+-- allocating a temporary encoded ByteString for every cell.+parameterTypeSignature :: MySQLValue -> Word16+parameterTypeSignature value = case value of+    MySQLDecimal _ -> 0x00+    MySQLInt8U _ -> 0x8001+    MySQLInt8 _ -> 0x01+    MySQLInt16U _ -> 0x8002+    MySQLInt16 _ -> 0x02+    MySQLInt32U _ -> 0x8003+    MySQLInt32 _ -> 0x03+    MySQLInt64U _ -> 0x8008+    MySQLInt64 _ -> 0x08+    MySQLFloat _ -> 0x04+    MySQLDouble _ -> 0x05+    MySQLYear _ -> 0x8002+    MySQLDateTime _ -> 0x0c+    MySQLTimeStamp _ -> 0x07+    MySQLDate _ -> 0x0a+    MySQLTime _ _ -> 0x0b+    MySQLBytes _ -> 0xfc+    MySQLGeometry _ -> 0xff+    MySQLBit _ -> 0x8008+    MySQLText _ -> 0xfe+    MySQLDateRaw _ -> 0x0a+    MySQLDateTimeRaw _ _ -> 0x0c+    MySQLTimeStampRaw _ _ -> 0x07+    MySQLNull -> 0x06+{-# INLINE parameterTypeSignature #-}++-- | Encode exactly the same signature used to validate bulk column homogeneity.+putParamMySQLType :: MySQLValue -> Put+putParamMySQLType = putWord16le . parameterTypeSignature+{-# INLINE putParamMySQLType #-}++--------------------------------------------------------------------------------+-- | Text protocol decoder+getTextField :: ColumnDef -> Get MySQLValue+getTextField = getTextFieldGeneral+{-# NOINLINE getTextField #-}++-- Consume the nullable length marker once on the common paths, avoiding a+-- separate peek and repeated Get wrappers for every field of every row.+nullableTextField :: ColumnDef -> Get MySQLValue+nullableTextField field+    | typ == mySQLTypeLongLong = do+        tag <- getWord8+        if tag==0xfb then pure MySQLNull else do+            len <- getLenEncIntAfterTag tag+            when (len>128) (fail "numeric field exceeds representable width")+            bytes <- getByteString len+            if flagUnsigned (columnFlags field)+                then maybe (fail "invalid unsigned integer") (pure . MySQLInt64U) (boundedInteger 0 18446744073709551615 bytes)+                else maybe (fail "invalid signed integer") (pure . MySQLInt64) (boundedInteger (-9223372036854775808) 9223372036854775807 bytes)+    | typ == mySQLTypeVarString || typ == mySQLTypeVarChar || typ == mySQLTypeString = do+        tag <- getWord8+        if tag==0xfb then pure MySQLNull else do+            len <- getLenEncIntAfterTag tag+            bytes <- getByteString len+            pure (decodeString bytes)+    | otherwise = do+        tag <- peek+        if tag==0xfb then skipN 1 >> pure MySQLNull else getTextFieldGeneral field+  where+    typ=columnType field+    charset=columnCharSet field+    decodeString bytes+        | charset==63 = MySQLBytes bytes+        | charset>=192 && charset<=247 || charset==33 || charset==45 || charset==46 || charset==76 || charset==83 || charset>=2048 && charset<2560 = MySQLText (T.decodeUtf8 bytes)+        | otherwise = case parseOnly (decodeColumnText charset bytes) B.empty of+            Left message -> throw (DecodePacketFailed B.empty 0 message)+            Right value -> value+{-# INLINE nullableTextField #-}++stringField :: Word16 -> Get MySQLValue+stringField charset+    | charset == 63 = MySQLBytes <$> getLenEncBytes+    | charset>=192 && charset<=247 || charset==33 || charset==45 || charset==46 || charset==76 || charset==83 || charset>=2048 && charset<2560 = MySQLText . T.decodeUtf8 <$> getLenEncBytes+    | otherwise = getLenEncBytes >>= decodeColumnText charset+{-# INLINE stringField #-}++-- | Decode one non-NULL prepared field using its metadata. The enclosing row+-- parser consumes the NULL bitmap; standalone callers must do so themselves.+getBinaryField :: ColumnDef -> Get MySQLValue+getBinaryField field+    | columnType field == mySQLTypeLongLong = if flagUnsigned (columnFlags field) then MySQLInt64U <$> getWord64le else MySQLInt64 <$> getInt64le+    | columnType field == mySQLTypeVarString || columnType field == mySQLTypeVarChar || columnType field == mySQLTypeString = stringField (columnCharSet field)+    | otherwise = getBinaryFieldGeneral field+{-# INLINE getBinaryField #-}++getTextFieldGeneral :: ColumnDef -> Get MySQLValue+getTextFieldGeneral f+    | t == mySQLTypeNull            = pure MySQLNull+    | t == mySQLTypeDecimal+        || t == mySQLTypeNewDecimal = feedLenEncBytes t MySQLDecimal fracLexer+    | t == mySQLTypeTiny            = if isUnsigned then feedLenEncBytes t MySQLInt8U (boundedInteger 0 255)+                                                    else feedLenEncBytes t MySQLInt8 (boundedInteger (-128) 127)+    | t == mySQLTypeShort           = if isUnsigned then feedLenEncBytes t MySQLInt16U (boundedInteger 0 65535)+                                                    else feedLenEncBytes t MySQLInt16 (boundedInteger (-32768) 32767)+    | t == mySQLTypeLong+        || t == mySQLTypeInt24      = if isUnsigned then feedLenEncBytes t MySQLInt32U (boundedInteger 0 (if t == mySQLTypeInt24 then 16777215 else 4294967295))+                                                    else feedLenEncBytes t MySQLInt32 (boundedInteger (if t == mySQLTypeInt24 then -8388608 else -2147483648) (if t == mySQLTypeInt24 then 8388607 else 2147483647))+    | t == mySQLTypeLongLong        = if isUnsigned then feedLenEncBytes t MySQLInt64U (boundedInteger 0 18446744073709551615)+                                                    else feedLenEncBytes t MySQLInt64 (boundedInteger (-9223372036854775808) 9223372036854775807)+    | t == mySQLTypeFloat           = feedLenEncBytes t MySQLFloat fracLexer+    | t == mySQLTypeDouble          = feedLenEncBytes t MySQLDouble fracLexer+    | t == mySQLTypeYear            = feedLenEncBytes t MySQLYear (boundedInteger 0 65535)+    | t == mySQLTypeTimestamp || t == mySQLTypeTimestamp2 = feedLenEncBytes t id (parseDateTime MySQLTimeStamp MySQLTimeStampRaw)+    | t == mySQLTypeDateTime || t == mySQLTypeDateTime2 = feedLenEncBytes t id (parseDateTime MySQLDateTime MySQLDateTimeRaw)+    | t == mySQLTypeDate || t == mySQLTypeNewDate = feedLenEncBytes t id (fmap dateValue . parseDateParts)+    | t == mySQLTypeTime+        || t == mySQLTypeTime2      = feedLenEncBytes t id $ \ bs ->+                                          guard (not (B.null bs)) >>+                                          if B.index bs 0 == 45  -- '-'+                                               then MySQLTime 1 <$> timeParser (B.drop 1 bs)+                                               else MySQLTime 0 <$> timeParser bs++    | t == mySQLTypeGeometry        = MySQLGeometry <$> getLenEncBytes+    | t == mySQLTypeVarChar+        || t == mySQLTypeEnum+        || t == mySQLTypeSet+        || t == mySQLTypeTinyBlob+        || t == mySQLTypeMediumBlob+        || t == mySQLTypeLongBlob+        || t == mySQLTypeBlob+        || t == mySQLTypeVarString+        || t == mySQLTypeString     = getLenEncBytes >>= decodeColumnText (columnCharSet f)++    | t == mySQLTypeBit             = MySQLBit <$> (getBits =<< getLenEncInt)++    | otherwise                     = fail $ "Database.MySQL.Protocol.MySQLValue: missing text decoder for " ++ show t+  where+    t = columnType f+    isUnsigned = flagUnsigned (columnFlags f)+    fracLexer bs = do+        (x,rest) <- LexFrac.readSigned LexFrac.readDecimal bs+        guard (B.null rest)+        pure x+    timeParser = parseTime 838+++feedLenEncBytes :: FieldType -> (t -> b) -> (ByteString -> Maybe t) -> Get b+feedLenEncBytes typ con parser = do+    len <- getLenEncInt+    when (len>128) (fail "scalar field exceeds representable width")+    bs <- getByteString len+    case parser bs of+        Just v -> return (con v)+        Nothing -> fail $ "Database.MySQL.Protocol.MySQLValue: parsing " ++ show typ ++ " failed, \+                          \input: " ++ BC.unpack bs+{-# INLINE feedLenEncBytes #-}++--------------------------------------------------------------------------------+-- | Text protocol encoder+putTextField :: MySQLValue -> Put+putTextField (MySQLDecimal    n) = putBuilder (formatScientificBuilder Fixed Nothing (normalize n))+putTextField (MySQLInt8U      n) = putBuilder (Textual.integral n)+putTextField (MySQLInt8       n) = putBuilder (Textual.integral n)+putTextField (MySQLInt16U     n) = putBuilder (Textual.integral n)+putTextField (MySQLInt16      n) = putBuilder (Textual.integral n)+putTextField (MySQLInt32U     n) = putBuilder (Textual.integral n)+putTextField (MySQLInt32      n) = putBuilder (Textual.integral n)+putTextField (MySQLInt64U     n) = putBuilder (Textual.integral n)+putTextField (MySQLInt64      n) = putBuilder (Textual.integral n)+putTextField (MySQLFloat      x) = putBuilder (Textual.float x)+putTextField (MySQLDouble     x) = putBuilder (Textual.double x)+putTextField (MySQLYear       n) = putBuilder (Textual.integral n)+putTextField (MySQLDateTime  dt) = putInQuotes $+                                      putByteString (BC.pack (formatTime defaultTimeLocale "%F %T%Q" dt))+putTextField (MySQLTimeStamp dt) = putInQuotes $+                                      putByteString (BC.pack (formatTime defaultTimeLocale "%F %T%Q" dt))+putTextField (MySQLDate       d) = putInQuotes $+                                      putByteString (BC.pack (formatTime defaultTimeLocale "%F" d))+putTextField (MySQLTime  sign t) = putInQuotes $ do+                                      when (sign == 1) (putCharUtf8 '-')+                                      putByteString (BC.pack (formatTime defaultTimeLocale "%T%Q" t))+                                      -- this works even for hour > 24+putTextField (MySQLGeometry  bs) = putInQuotes $ putByteString . escapeBytes $ bs+putTextField (MySQLBytes     bs) = putInQuotes $ putByteString . escapeBytes $ bs+putTextField (MySQLText       t) = putInQuotes $+                                      putByteString . T.encodeUtf8 . escapeText $ t+putTextField (MySQLBit        b) = do putBuilder "b\'"+                                      putBuilder . execPut $ putTextBits b+                                      putCharUtf8 '\''+  where+    putTextBits :: Word64 -> Put+    putTextBits word = forM_ [63,62..0] $ \ pos ->+            if word `testBit` pos then putCharUtf8 '1' else putCharUtf8 '0'+    {-# INLINE putTextBits #-}++putTextField (MySQLDateRaw d) = putInQuotes (putRawDate d)+putTextField (MySQLDateTimeRaw d t) = putInQuotes (putRawDate d >> putWord8 32 >> putByteString (BC.pack (formatTime defaultTimeLocale "%T%Q" t)))+putTextField (MySQLTimeStampRaw d t) = putTextField (MySQLDateTimeRaw d t)+putTextField MySQLNull           = putBuilder "NULL"++putInQuotes :: Put -> Put+putInQuotes p = putCharUtf8 '\'' >> p >> putCharUtf8 '\''+{-# INLINE putInQuotes #-}++--------------------------------------------------------------------------------+-- | Text row decoder+getTextRow :: [ColumnDef] -> Get [MySQLValue]+getTextRow fs = mapM nullableTextField fs+{-# INLINE getTextRow #-}++-- | Vector form of 'getTextRow', including each field's nullable length tag.+getTextRowVector :: V.Vector ColumnDef -> Get (V.Vector MySQLValue)+getTextRowVector fs = V.mapM nullableTextField fs+{-# INLINE getTextRowVector #-}++--------------------------------------------------------------------------------+-- | Binary protocol decoder+getBinaryFieldGeneral :: ColumnDef -> Get MySQLValue+getBinaryFieldGeneral f+    | t == mySQLTypeNull              = pure MySQLNull+    | t == mySQLTypeDecimal+        || t == mySQLTypeNewDecimal   = feedLenEncBytes t MySQLDecimal fracLexer+    | t == mySQLTypeTiny              = if isUnsigned then MySQLInt8U <$> getWord8+                                                      else MySQLInt8  <$> getInt8+    | t == mySQLTypeShort             = if isUnsigned then MySQLInt16U <$> getWord16le+                                                      else MySQLInt16  <$> getInt16le+    | t == mySQLTypeLong+        || t == mySQLTypeInt24        = if isUnsigned then MySQLInt32U <$> getWord32le+                                                      else MySQLInt32  <$> getInt32le+    | t == mySQLTypeYear              = MySQLYear <$> getWord16le+    | t == mySQLTypeLongLong          = if isUnsigned then MySQLInt64U <$> getWord64le+                                                      else MySQLInt64  <$> getInt64le+    | t == mySQLTypeFloat             = MySQLFloat  <$> getFloatle+    | t == mySQLTypeDouble            = MySQLDouble <$> getDoublele+    | t == mySQLTypeTimestamp || t == mySQLTypeTimestamp2 = binaryDateTime MySQLTimeStamp MySQLTimeStampRaw+    | t == mySQLTypeDateTime || t == mySQLTypeDateTime2 = binaryDateTime MySQLDateTime MySQLDateTimeRaw+    | t == mySQLTypeDate || t == mySQLTypeNewDate = do+        n <- getWord8+        case n of+            0 -> pure (MySQLDateRaw (DateParts 0 0 0))+            4 -> dateValue <$> binaryDateParts+            _ -> fail "invalid binary DATE length"+    | t == mySQLTypeTime+        || t == mySQLTypeTime2        = do+            n <- getLenEncInt+            case n of+               0 -> pure $ MySQLTime 0 (TimeOfDay 0 0 0)+               8 -> do+                   sign <- getWord8+                   unless (sign<=1) (fail "invalid TIME sign")+                   wireDays <- getWord32le+                   when (wireDays>34) (fail "invalid TIME days")+                   let d=fromIntegral wireDays+                   h <- getInt8'+                   minute <- getInt8'+                   seconds <- getSecond4+                   unless (h<24 && d*24+h<=838 && minute<60) (fail "invalid TIME duration")+                   pure (MySQLTime sign (TimeOfDay (d*24+h) minute seconds))++               12 -> do+                   sign <- getWord8+                   unless (sign<=1) (fail "invalid TIME sign")+                   wireDays <- getWord32le+                   when (wireDays>34) (fail "invalid TIME days")+                   let d=fromIntegral wireDays+                   h <- getInt8'+                   minute <- getInt8'+                   seconds <- getSecond8+                   unless (h<24 && d*24+h<=838 && minute<60) (fail "invalid TIME duration")+                   pure (MySQLTime sign (TimeOfDay (d*24+h) minute seconds))+               _ -> fail "Database.MySQL.Protocol.MySQLValue: wrong TIME length"++    | t == mySQLTypeGeometry          = MySQLGeometry <$> getLenEncBytes+    | t == mySQLTypeVarChar+        || t == mySQLTypeEnum+        || t == mySQLTypeSet+        || t == mySQLTypeTinyBlob+        || t == mySQLTypeMediumBlob+        || t == mySQLTypeLongBlob+        || t == mySQLTypeBlob+        || t == mySQLTypeVarString+        || t == mySQLTypeString       = getLenEncBytes >>= decodeColumnText (columnCharSet f)+    | t == mySQLTypeBit               = MySQLBit <$> (getBits =<< getLenEncInt)+    | otherwise                       = fail $ "Database.MySQL.Protocol.MySQLValue:\+                                               \ missing binary decoder for " ++ show t+  where+    t = columnType f+    isUnsigned = flagUnsigned (columnFlags f)+    fracLexer bs = do+        (x,rest) <- LexFrac.readSigned LexFrac.readDecimal bs+        guard (B.null rest)+        pure x+    getInt8' :: Get Int+    getInt8' = fromIntegral <$> getWord8+    getSecond4 :: Get Pico+    getSecond4 = do+        value <- getWord8+        if value<60 then pure (fromIntegral value) else fail "invalid seconds"+    getSecond8 :: Get Pico+    getSecond8 =  do+        s <- getInt8'+        -- Validate on the wire's unsigned type. Converting Word32 to Int first+        -- can turn a large malformed value negative on a 32-bit host, letting+        -- it pass the upper-bound check and produce negative fractional time.+        ms <- getWord32le+        unless (s<60 && ms<1000000) (fail "invalid fractional seconds")+        pure $! (realToFrac s + realToFrac ms / 1000000 :: Pico)+++-- | Get a bit sequence as a Word64+--+-- Since 'Word64' has a @Bits@ instance, it's easier to deal with in haskell.+--+getBits :: Int -> Get Word64+getBits bytes =+    if  | bytes == 1 -> fromIntegral <$> getWord8+        | bytes == 2 -> fromIntegral <$> getWord16be+        | bytes == 3 -> fromIntegral <$> getWord24be+        | bytes == 4 -> fromIntegral <$> getWord32be+        | bytes == 5 -> getWord40be+        | bytes == 6 -> getWord48be+        | bytes == 7 -> getWord56be+        | bytes == 8 -> getWord64be+        | otherwise  -> fail $  "Database.MySQL.Protocol.MySQLValue: \+                                \wrong bit length size: " ++ show bytes+{-# INLINE getBits #-}+++--------------------------------------------------------------------------------+-- | Binary protocol encoder+putBinaryField :: MySQLValue -> Put+putBinaryField (MySQLDecimal    n) = putLenEncBytes . L.toStrict . BB.toLazyByteString $+                                        formatScientificBuilder Fixed Nothing (normalize n)+putBinaryField (MySQLInt8U      n) = putWord8 n+putBinaryField (MySQLInt8       n) = putWord8 (fromIntegral n)+putBinaryField (MySQLInt16U     n) = putWord16le n+putBinaryField (MySQLInt16      n) = putInt16le n+putBinaryField (MySQLInt32U     n) = putWord32le n+putBinaryField (MySQLInt32      n) = putInt32le n+putBinaryField (MySQLInt64U     n) = putWord64le n+putBinaryField (MySQLInt64      n) = putInt64le n+putBinaryField (MySQLFloat      x) = putFloatle x+putBinaryField (MySQLDouble     x) = putDoublele x+putBinaryField (MySQLYear       n) = putWord16le n+putBinaryField (MySQLTimeStamp (LocalTime date time)) = do putWord8 11    -- always put full+                                                           putBinaryDay date+                                                           putBinaryTime' time+putBinaryField (MySQLDateTime  (LocalTime date time)) = do putWord8 11    -- always put full+                                                           putBinaryDay date+                                                           putBinaryTime' time+putBinaryField (MySQLDate    d)    = do putWord8 4+                                        putBinaryDay d+putBinaryField (MySQLTime sign t)  = do putWord8 12    -- always put full+                                        putWord8 sign+                                        putBinaryTime t+putBinaryField (MySQLGeometry bs)  = putLenEncBytes bs+putBinaryField (MySQLBytes  bs)    = putLenEncBytes bs+putBinaryField (MySQLBit    word)  = putWord64le word+putBinaryField (MySQLText    t)    = putLenEncBytes (T.encodeUtf8 t)+putBinaryField (MySQLDateRaw d) = putWord8 4 >> putBinaryDateParts d+putBinaryField (MySQLDateTimeRaw d t) = putWord8 11 >> putBinaryDateParts d >> putBinaryTime' t+putBinaryField (MySQLTimeStampRaw d t) = putBinaryField (MySQLDateTimeRaw d t)+putBinaryField MySQLNull           = return ()++putBinaryDay :: Day -> Put+putBinaryDay d = do let (yyyy, mm, dd) = toGregorian d+                    putWord16le (fromIntegral yyyy)+                    putWord8 (fromIntegral mm)+                    putWord8 (fromIntegral dd)+{-# INLINE putBinaryDay #-}++putBinaryTime' :: TimeOfDay -> Put+putBinaryTime' (TimeOfDay hh mm ss) = do let s = floor ss+                                             ms = floor $ (ss - realToFrac s) * 1000000+                                         putWord8 (fromIntegral hh)+                                         putWord8 (fromIntegral mm)+                                         putWord8 s+                                         putWord32le ms+{-# INLINE putBinaryTime' #-}++putBinaryTime :: TimeOfDay -> Put+putBinaryTime (TimeOfDay hh mm ss) = do let s = floor ss+                                            ms = floor $ (ss - realToFrac s) * 1000000+                                            (d, h) = hh `quotRem` 24  -- hour may exceed 24 here+                                        putWord32le (fromIntegral d)+                                        putWord8 (fromIntegral h)+                                        putWord8 (fromIntegral mm)+                                        putWord8 s+                                        putWord32le ms+{-# INLINE putBinaryTime #-}++--------------------------------------------------------------------------------+-- | Binary row decoder+--+-- MySQL use a special null bitmap without offset = 2 here.+--+getBinaryRow :: [ColumnDef] -> Int -> Get [MySQLValue]+getBinaryRow fields flen = do+    skipN 1           -- 0x00+    let maplen = (flen + 7 + 2) `shiftR` 3+    nullmap <- BitMap <$> getByteString maplen+    go fields nullmap 0+  where+    go :: [ColumnDef] -> BitMap -> Int -> Get [MySQLValue]+    go []     _       _   = pure []+    go (f:fs) nullmap pos = do+        r <- if isColumnNull nullmap pos+                then return MySQLNull+                else getBinaryField f+        let pos' = pos + 1+        rest <- pos' `seq` go fs nullmap pos'+        return (r `seq` (r : rest))+{-# INLINE getBinaryRow #-}++-- | Vector form of 'getBinaryRow'. The count must equal the metadata length;+-- the enclosing packet phase must already establish the binary row marker.+getBinaryRowVector :: V.Vector ColumnDef -> Int -> Get (V.Vector MySQLValue)+getBinaryRowVector fields flen = do+    skipN 1           -- 0x00+    let maplen = (flen + 7 + 2) `shiftR` 3+    nullmap <- BitMap <$> getByteString maplen+    (`V.imapM` fields) $ \ pos f ->+        if isColumnNull nullmap pos then return MySQLNull else getBinaryField f+{-# INLINE getBinaryRowVector #-}++--------------------------------------------------------------------------------+-- | Use 'ByteString' to present a bitmap.+--+-- When used for represent bits values, the underlining 'ByteString' follows:+--+--  * byteString: head       -> tail+--  * bit:        high bit   -> low bit+--+-- When used as a null-map/present-map, every bit inside a byte+-- is mapped to a column, the mapping order is following:+--+--  * byteString: head -> tail+--  * column:     left -> right+--+-- We don't use 'Int64' here because there maybe more than 64 columns.+--+newtype BitMap = BitMap { fromBitMap :: ByteString } deriving (Eq, Show)++-- | Test if a column is set(binlog protocol).+--+-- The number counts from left to right.+--+isColumnSet :: BitMap -> Int -> Bool+isColumnSet (BitMap bitmap) pos =+  let i = pos `unsafeShiftR` 3+      j = pos .&. 7+  in (bitmap `B.unsafeIndex` i) `testBit` j+{-# INLINE isColumnSet #-}++-- | Test if a column is null(binary protocol).+--+-- The number counts from left to right.+--+isColumnNull :: BitMap -> Int -> Bool+isColumnNull (BitMap nullmap) pos =+  let+    pos' = pos + 2+    i    = pos' `unsafeShiftR` 3+    j    = pos' .&. 7+  in (nullmap `B.unsafeIndex` i) `testBit` j+{-# INLINE isColumnNull #-}++-- | Make a nullmap for params(binary protocol) without offset.+--+makeNullMap :: [MySQLValue] -> BitMap+makeNullMap values = BitMap . B.pack $ go values 0x00 0+  where+    go :: [MySQLValue] -> Word8 -> Int -> [Word8]+    go []             byte   8  = [byte]+    go vs             byte   8  = byte : go vs 0x00 0+    go []             byte   _  = [byte]+    go (MySQLNull:vs) byte pos  = let pos' = pos + 1+                                      byte' = byte .|. bit pos+                                  in pos' `seq` byte' `seq` go vs byte' pos'+    go (_        :vs) byte pos  = let pos' = pos + 1 in pos' `seq` go vs byte pos'++--------------------------------------------------------------------------------+-- TODO: add helpers to parse mySQLTypeGEOMETRY+-- reference: https://github.com/felixge/node-mysql/blob/master/lib/protocol/Parser.js++-- Reject overflow before multiplying the Word64 accumulator. A negative Int64+-- minimum needs magnitude 2^63, which fits Word64 even though it cannot fit an+-- Int64 intermediate. The bounds at each call site also cover MEDIUMINT widths.+boundedInteger :: Integral a => Integer -> Integer -> ByteString -> Maybe a+boundedInteger low high bs = do+    guard (not (B.null bs))+    let negative = B.head bs == 45+        positive = B.head bs == 43+        digits = if negative || positive then B.tail bs else bs+        bound = fromInteger (if negative then negate low else high) :: Word64+    guard (not (B.null digits) && (not negative || low < 0))+    let quotient = bound `quot` 10+        remainder = bound `rem` 10+        loop !index !acc+            | index == B.length digits = Just (if negative then negate (fromIntegral acc) else fromIntegral acc)+            | otherwise =+                let w=B.unsafeIndex digits index+                    digit=fromIntegral (w-48)+                in if w<48 || w>57 || acc>quotient || (acc==quotient && digit>remainder)+                   then Nothing else loop (index+1) (acc*10+digit)+    loop 0 (0::Word64)+{-# INLINE boundedInteger #-}++parseDateParts :: ByteString -> Maybe DateParts+parseDateParts bs = do+    guard (B.length bs == 10 && B.index bs 4 == 45 && B.index bs 7 == 45)+    guard (B.all (\w -> w>=48 && w<=57 || w==45) bs)+    y <- boundedInteger 0 9999 (B.take 4 bs)+    m <- boundedInteger 0 12 (B.take 2 (B.drop 5 bs))+    d <- boundedInteger 0 31 (B.drop 8 bs)+    pure (DateParts y m d)++dateValue :: DateParts -> MySQLValue+dateValue d@(DateParts y m day) = maybe (MySQLDateRaw d) MySQLDate (fromGregorianValid y m day)++parseTime :: Int -> ByteString -> Maybe TimeOfDay+parseTime maxHour bs = do+    (h,rest) <- LexInt.readDecimal bs+    let hoursLength=B.length bs-B.length rest+        secondsBytes=B.drop 4 rest+        (secondsInteger,fraction)=B.break (==46) secondsBytes+        digitsOnly=B.all (\w -> w>=48 && w<=57)+    guard (hoursLength>=2 && hoursLength<=(if maxHour<24 then 2 else 3) && h <= maxHour && B.length rest >= 6 && B.head rest == 58 && B.index rest 3 == 58)+    guard (B.length secondsInteger==2 && digitsOnly secondsInteger && (B.null fraction || B.length fraction>=2 && B.length fraction<=7 && digitsOnly (B.tail fraction)))+    m <- boundedInteger 0 59 (B.take 2 (B.drop 1 rest))+    (sec,tailBytes) <- LexFrac.readDecimal (B.drop 4 rest)+    guard (B.null tailBytes && sec >= 0 && sec < 60)+    pure (TimeOfDay h m sec)++parseDateTime :: (LocalTime -> MySQLValue) -> (DateParts -> TimeOfDay -> MySQLValue) -> ByteString -> Maybe MySQLValue+parseDateTime valid raw bs = do+    guard (B.length bs >= 19 && B.index bs 10 == 32)+    d@(DateParts y m day) <- parseDateParts (B.take 10 bs)+    t <- parseTime 23 (B.drop 11 bs)+    pure (maybe (raw d t) (valid . (`LocalTime` t)) (fromGregorianValid y m day))++binaryDateParts :: Get DateParts+binaryDateParts = do+    y <- fromIntegral <$> getWord16le+    m <- fromIntegral <$> getWord8+    d <- fromIntegral <$> getWord8+    unless (y <= 9999 && m <= 12 && d <= 31) (fail "invalid binary date components")+    pure (DateParts y m d)++binaryDateTime :: (LocalTime -> MySQLValue) -> (DateParts -> TimeOfDay -> MySQLValue) -> Get MySQLValue+binaryDateTime valid raw = do+    n <- getWord8+    unless (n `elem` [0,4,7,11]) (fail "invalid binary datetime length")+    d@(DateParts y m day) <- if n == 0 then pure (DateParts 0 0 0) else binaryDateParts+    t <- if n < 7 then pure (TimeOfDay 0 0 0) else do+        h <- fromIntegral <$> getWord8+        minute <- fromIntegral <$> getWord8+        sec <- fromIntegral <$> getWord8+        micro <- if n == 11 then getWord32le else pure 0+        unless (h < 24 && minute < 60 && sec < 60 && micro < 1000000) (fail "invalid binary clock")+        pure (TimeOfDay h minute (sec + fromIntegral micro / 1000000))+    pure (maybe (raw d t) (valid . (`LocalTime` t)) (fromGregorianValid y m day))++putBinaryDateParts :: DateParts -> Put+putBinaryDateParts (DateParts y m d) = putWord16le (fromIntegral y) >> putWord8 (fromIntegral m) >> putWord8 (fromIntegral d)++putRawDate :: DateParts -> Put+putRawDate (DateParts y m d) = putByteString (BC.pack (pad 4 y ++ "-" ++ pad 2 (fromIntegral m) ++ "-" ++ pad 2 (fromIntegral d)))+  where+    pad :: Int -> Integer -> String+    pad width x = let v = show x in replicate (max 0 (width-length v)) '0' ++ v++-- MariaDB latin1 uses the server's cp1252 mapping, including undefined controls.+cp1252 :: Word8 -> Char+cp1252 w | w >= 128 && w <= 159 = chr (cp1252High !! (fromIntegral w-128))+          | otherwise = chr (fromIntegral w)++cp1252High :: [Int]+cp1252High = [0x20ac,0x81,0x201a,0x192,0x201e,0x2026,0x2020,0x2021,0x2c6,0x2030,0x160,0x2039,0x152,0x8d,0x17d,0x8f,0x90,0x2018,0x2019,0x201c,0x201d,0x2022,0x2013,0x2014,0x2dc,0x2122,0x161,0x203a,0x153,0x9d,0x17e,0x178]++-- | Decode by the column's collation ID, not by the connection default. UTF-8+-- is strict; MariaDB latin1 uses cp1252. Binary and unmapped encodings return+-- exact bytes so unsupported text cannot be silently replaced or mistranslated.+-- The high ID range covers MariaDB's UCA-1400 UTF-8 collations.+decodeColumnText :: Word16 -> ByteString -> Get MySQLValue+decodeColumnText col bytes+    | col == 63 = pure (MySQLBytes bytes)+    | col `elem` [5,8,15,31,47,48,49,94] = pure (MySQLText (Text.pack (map cp1252 (B.unpack bytes))))+    | col `elem` [11,65] = if B.all (<128) bytes then pure (MySQLText (T.decodeUtf8 bytes)) else fail "non-ASCII data in ASCII column"+    | col `elem` [33,45,46,76,83] || (col >= 192 && col <= 247) || (col >= 256 && col <= 323) || (col >= 2048 && col < 2560) =+        pure $! MySQLText (T.decodeUtf8 bytes)+    | otherwise = pure (MySQLBytes bytes)+{-# INLINE decodeColumnText #-}++-- | Encode a parameter in the verified client charset. Fail on unrepresentable+-- characters, including supplementary Unicode under utf8mb3; never substitute.+encodeCharsetText :: ByteString -> Text -> Either String ByteString+encodeCharsetText charset value+    | charset `elem` ["utf8mb4","utf8mb3","utf8"] =+        if charset /= "utf8mb4" && Text.any ((>65535) . ord) value then Left "character outside utf8mb3" else Right (T.encodeUtf8 value)+    | charset == "ascii" = if Text.all ((<128) . ord) value then Right (B.pack (map (fromIntegral . ord) (Text.unpack value))) else Left "character outside ASCII"+    | charset == "latin1" = B.pack <$> traverse encode (Text.unpack value)+    | otherwise = Left "unsupported client character set"+  where+    encode ch | fromEnum ch<128 || fromEnum ch>=160 && fromEnum ch<=255 = Right (fromIntegral (fromEnum ch))+    encode ch = case [fromIntegral n | n <- [128..159::Int], cp1252 (fromIntegral n) == ch] of+        x:_ -> Right x+        [] -> Left "character outside MariaDB latin1"++-- | Build field dispatch once per result and share it across rows. Metadata is+-- not cached across executions, where DDL/reprepare may have changed its shape.+prepareTextRow :: [ColumnDef] -> Get [MySQLValue]+prepareTextRow fields = sequencePrepared (map nullableTextField fields)+{-# NOINLINE prepareTextRow #-}++sequencePrepared :: [Get a] -> Get [a]+sequencePrepared = foldr (\parser rest -> parser `seq` rest `seq` ((:) <$> parser <*> rest)) (pure [])++-- | Vector counterpart of 'prepareTextRow'.+prepareTextRowVector :: V.Vector ColumnDef -> Get (V.Vector MySQLValue)+prepareTextRowVector fields = V.sequence (V.map nullableTextField fields)+{-# NOINLINE prepareTextRowVector #-}++-- | Share binary field parsers per result. The row's NULL bitmap starts at bit+-- two; an all-zero bitmap can use a prebuilt non-null sequence. Force dispatch+-- here, then force decoded values at the guarded I/O boundary in Base. Adding+-- per-field wrappers/forcing to the hot loop increased allocation in profiling.+prepareBinaryRow :: [ColumnDef] -> Get [MySQLValue]+prepareBinaryRow fields =+    let parsers=map getBinaryField fields+        !count=length fields+        nonNull=sequencePrepared parsers+        force []=()+        force (p:ps)=p `seq` force ps+    in force parsers `seq` nonNull `seq` do+        marker <- getWord8+        unless (marker==0) (fail "invalid binary row marker")+        nullBytes <- getByteString ((count+9) `shiftR` 3)+        let bitmap=BitMap nullBytes+        let go [] _=pure []+            go (p:ps) !position = do+                value <- if isColumnNull bitmap position then pure MySQLNull else p+                (value:) <$> go ps (position+1)+        if B.all (==0) nullBytes then nonNull else go parsers 0+{-# NOINLINE prepareBinaryRow #-}++-- | Vector counterpart of 'prepareBinaryRow', with the same two-bit NULL offset.+prepareBinaryRowVector :: V.Vector ColumnDef -> Get (V.Vector MySQLValue)+prepareBinaryRowVector fields =+    let parsers=V.map getBinaryField fields+        !count=V.length fields+    in V.foldl' (\() parser -> parser `seq` ()) () parsers `seq` do+        marker <- getWord8+        unless (marker==0) (fail "invalid binary row marker")+        bitmap <- BitMap <$> getByteString ((count+9) `shiftR` 3)+        V.imapM (\position parser -> if isColumnNull bitmap position then pure MySQLNull else parser) parsers+{-# NOINLINE prepareBinaryRowVector #-}++-- | Reject lossy/invalid built-in parameters before a command starts writing.+-- Raw dates need valid component ranges, not a valid Gregorian calendar date.+-- This validates wire representability; the server still enforces column type,+-- SQL mode and constraints. Low-level Put encoders assume this check was done.+validateValue :: MySQLValue -> Either String ()+validateValue value = case value of+    MySQLDate day -> calendar day+    MySQLDateTime (LocalTime day time) -> calendar day >> clock 23 time+    MySQLTimeStamp (LocalTime day time) -> calendar day >> clock 23 time+    MySQLDateRaw parts -> date parts+    MySQLDateTimeRaw parts time -> date parts >> clock 23 time+    MySQLTimeStampRaw parts time -> date parts >> clock 23 time+    MySQLTime sign time -> check (sign<=1) "invalid TIME sign" >> clock 838 time+    MySQLFloat x -> check (not (isNaN x || isInfinite x)) "non-finite FLOAT"+    MySQLDouble x -> check (not (isNaN x || isInfinite x)) "non-finite DOUBLE"+    MySQLDecimal x ->+        let n = normalize x+            c = abs (coefficient n)+            decimalExponent = base10Exponent n+        in check (c < 10^(65::Int) && decimalExponent >= (-38) && decimalExponent <= 65+                  && length (show c) + max 0 decimalExponent <= 65)+                 "DECIMAL exceeds MariaDB precision/scale"+    _ -> Right ()+  where+    check True _=Right ()+    check False message=Left message+    date (DateParts y m d)=check (y>=0 && y<=9999 && m>=0 && m<=12 && d>=0 && d<=31) "date component outside MariaDB range"+    calendar day=let (y,m,d)=toGregorian day in date (DateParts y m d)+    clock maximumHour (TimeOfDay h m sec)=check (h>=0 && h<=maximumHour && m>=0 && m<60 && sec>=0 && sec<60 && sec*1000000==fromInteger (floor (sec*1000000))) "invalid clock/duration or sub-microsecond precision"
+ src/Database/MySQL/Protocol/Packet.hs view
@@ -0,0 +1,357 @@+{-# OPTIONS_GHC -funbox-strict-fields #-}++{-|+Module      : Database.MySQL.Protocol.Packet+Description : MariaDB packet framing, replies and limits+Copyright   : (c) Winterland, 2016+License     : BSD+Maintainer  : Anton Antich <anton.antich@gmail.com>+Stability   : experimental+Portability : PORTABLE++MySQL packet decoder&encoder, and varities utility.++-}++module Database.MySQL.Protocol.Packet where++import           Control.Exception     (Exception (..), throwIO)+import           Data.Binary.Parser+import           Data.Binary.Put+import           Data.Binary           (Binary(..), encode)+import           Data.Bits+import qualified Data.ByteString       as B+import           Data.ByteString.Char8 as BC+import qualified Data.ByteString.Lazy  as L+import           Data.Int.Int24+import           Data.Int+import           Data.Word+import           Data.Typeable+import           Data.Word.Word24++--------------------------------------------------------------------------------+-- | MySQL packet type+--+data Packet = Packet+    { pLen  :: !Int64+    , pSeqN :: !Word8+    , pBody :: !L.ByteString+    } deriving (Show, Eq)++putPacket :: Packet -> Put+putPacket (Packet len seqN body)  = do+    putWord24le (fromIntegral len)+    putWord8 seqN+    putLazyByteString body+{-# INLINE putPacket #-}++getPacket :: Get Packet+getPacket = do+    len <- fromIntegral <$> getWord24le+    seqN <- getWord8+    body <- getLazyByteString len+    return (Packet len seqN body)+{-# INLINE getPacket #-}++instance Binary Packet where+    put = putPacket+    {-# INLINE put #-}+    get = getPacket+    {-# INLINE get #-}++isERR :: Packet -> Bool+isERR p = not (L.null (pBody p)) && L.head (pBody p) == 0xFF+{-# INLINE isERR #-}++isOK :: Packet -> Bool+isOK p = not (L.null (pBody p)) && L.head (pBody p) == 0x00+{-# INLINE isOK #-}++-- | Recognize a short legacy EOF. The 0xfe marker also starts an eight-byte+-- length-encoded row value, so checking its first byte alone would truncate+-- large rows. Negotiated OK terminators are handled by the connection layer.+isEOF :: Packet -> Bool+isEOF p = pLen p > 0 && pLen p < 9 && not (L.null (pBody p)) && L.head (pBody p) == 0xFE+{-# INLINE isEOF #-}++-- | Is this an AuthMoreData packet? (first byte 0x01)+-- Used during authentication handshake for caching_sha2_password.+isAuthMoreData :: Packet -> Bool+isAuthMoreData p = not (L.null (pBody p)) && L.head (pBody p) == 0x01+{-# INLINE isAuthMoreData #-}++-- | Is this an AuthSwitchRequest packet? (first byte 0xFE)+-- Same marker as EOF but used in authentication context.+isAuthSwitch :: Packet -> Bool+isAuthSwitch p = not (L.null (pBody p)) && L.head (pBody p) == 0xFE+{-# INLINE isAuthSwitch #-}++-- | Is there more packet to be read?+--+--  https://dev.mysql.com/doc/internals/en/status-flags.html+isThereMore :: OK -> Bool+isThereMore p  = okStatus p .&. 0x08 /= 0+{-# INLINE isThereMore #-}++-- | Decode a Binary payload in IO, permitting a separately parsed trailer.+-- Fixed row/metadata structures should use 'getFromPacketExact' instead.+decodeFromPacket :: Binary a => Packet -> IO a+decodeFromPacket = getFromPacket get+{-# INLINE decodeFromPacket #-}++-- | Decode with a supplied parser, permitting unconsumed bytes and forcing the+-- result to WHNF. Row readers additionally force their values under ownership;+-- WHNF alone does not force every element of a returned list.+getFromPacket :: Get a -> Packet -> IO a+getFromPacket g (Packet _ _ body) = case parseDetailLazy g body of+    Left  (buf, offset, errmsg) -> throwIO (DecodePacketFailed buf offset errmsg)+    Right (_,   _,      r     ) -> return $! r+{-# INLINE getFromPacket #-}++-- | A binary parser rejected a packet, with its buffered bytes, consumed offset+-- and parser message. Live command/stream guards retire an incomplete exchange;+-- low-level callers decide how to handle independent packet data.+data DecodePacketException = DecodePacketFailed ByteString ByteOffset String+  deriving (Typeable, Show)+instance Exception DecodePacketException++encodeToPacket :: Binary a => Word8 -> a -> Packet+encodeToPacket seqN payload =+    let s = encode payload+        l = L.length s+    in Packet l seqN s+{-# INLINE encodeToPacket #-}++putToPacket :: Word8 -> Put -> Packet+putToPacket seqN payload =+    let s = runPut payload+        l = L.length s+    in Packet l seqN s+{-# INLINE putToPacket #-}++--------------------------------------------------------------------------------+-- OK, ERR, EOF++-- | You may get interested in t'OK' packet because it provides information about+-- successful operations.+--+data OK = OK+    { okAffectedRows :: !Word64      -- ^ affected row number+    , okLastInsertID :: !Word64      -- ^ last insert's ID+    , okStatus       :: !Word16+    , okWarningCnt   :: !Word16+    } deriving (Show, Eq)++-- | Decode the fixed OK prefix after its marker has been identified. Optional+-- info/session-tracking trailers are consumed by the connection layer.+getOK :: Get OK+getOK = OK <$ skipN 1+           <*> getLenEncWord64+           <*> getLenEncWord64+           <*> getWord16le+           <*> getWord16le+{-# INLINE getOK #-}++-- | Encode the fixed OK prefix; negotiated tracking envelopes are separate.+putOK :: OK -> Put+putOK (OK row lid stat wcnt) = do+    putWord8 0x00+    putLenEncWord64 row+    putLenEncWord64 lid+    putWord16le stat+    putWord16le wcnt+{-# INLINE putOK #-}++instance Binary OK where+    get = getOK+    {-# INLINE get #-}+    put = putOK+    {-# INLINE put #-}++data ERR = ERR+    { errCode  :: !Word16+    , errState :: !ByteString+    , errMsg   :: !ByteString+    } deriving (Show, Eq)++getERR :: Get ERR+getERR = ERR <$  skipN 1+             <*> getWord16le+             <*  skipN 1+             <*> getByteString 5+             <*> getRemainingByteString+{-# INLINE getERR #-}++putERR :: ERR -> Put+putERR (ERR code stat msg) = do+    putWord8 0xFF+    putWord16le code+    putWord8 35 -- '#'+    putByteString stat+    putByteString msg+{-# INLINE putERR #-}++instance Binary ERR where+    get = getERR+    {-# INLINE get #-}+    put = putERR+    {-# INLINE put #-}++data EOF = EOF+    { eofWarningCnt :: !Word16+    , eofStatus     :: !Word16+    } deriving (Show, Eq)++getEOF :: Get EOF+getEOF = EOF <$  skipN 1+             <*> getWord16le+             <*> getWord16le+{-# INLINE getEOF #-}++putEOF :: EOF -> Put+putEOF (EOF wcnt stat) = do+    putWord8 0xFE+    putWord16le wcnt+    putWord16le stat+{-# INLINE putEOF #-}++instance Binary EOF where+    get = getEOF+    {-# INLINE get #-}+    put = putEOF+    {-# INLINE put #-}++--------------------------------------------------------------------------------+--  Helpers++getByteStringNul :: Get ByteString+getByteStringNul = L.toStrict <$> getLazyByteStringNul+{-# INLINE getByteStringNul #-}++getRemainingByteString :: Get ByteString+getRemainingByteString = L.toStrict <$> getRemainingLazyByteString+{-# INLINE getRemainingByteString #-}++putLenEncBytes :: ByteString -> Put+putLenEncBytes c = do+    putLenEncInt (B.length c)+    putByteString c+{-# INLINE putLenEncBytes #-}++getLenEncBytes :: Get ByteString+getLenEncBytes = getLenEncInt >>= getByteString+{-# INLINE getLenEncBytes #-}++-- | Decode a non-NULL unsigned length-encoded value. OK counters and insert IDs+-- use the full Word64 range even when the host's Int is narrower.+getLenEncWord64 :: Get Word64+getLenEncWord64 = do+    tag <- getWord8+    case tag of+        0xfc -> fromIntegral <$> getWord16le+        0xfd -> fromIntegral <$> getWord24le+        0xfe -> getWord64le+        _ | tag < 0xfb -> pure (fromIntegral tag)+          | otherwise -> fail "NULL or invalid non-null length integer"+{-# INLINE getLenEncWord64 #-}++-- | Decode a length that will index memory, rejecting values outside host Int.+getLenEncInt :: Get Int+getLenEncInt = getWord8 >>= getLenEncIntAfterTag++-- | Length decoder after a row parser has already consumed its NULL/length tag.+-- Sharing that read avoids a second peek and Get wrapper on every text field.+getLenEncIntAfterTag :: Word8 -> Get Int+getLenEncIntAfterTag tag+    | tag < 0xfb = pure (fromIntegral tag)+    | tag == 0xfc = fromIntegral <$> getWord16le+    | tag == 0xfd = fromIntegral <$> getWord24le+    | tag == 0xfe = do+        n <- getWord64le+        if n > fromIntegral (maxBound :: Int) then fail "length exceeds machine Int" else pure (fromIntegral n)+    | otherwise = fail "NULL or invalid non-null length integer"+{-# INLINE getLenEncIntAfterTag #-}+{-# INLINE getLenEncInt #-}++-- | Encode a non-NULL unsigned value using the shortest protocol representation.+putLenEncWord64 :: Word64 -> Put+putLenEncWord64 n+    | n < 251 = putWord8 (fromIntegral n)+    | n < 65536 = putWord8 0xfc >> putWord16le (fromIntegral n)+    | n < 16777216 = putWord8 0xfd >> putWord24le (fromIntegral n)+    | otherwise = putWord8 0xfe >> putWord64le n+{-# INLINE putLenEncWord64 #-}++putLenEncInt :: Int -> Put+putLenEncInt n | n < 0 = error "negative protocol length"+               | otherwise = putLenEncWord64 (fromIntegral n)+{-# INLINE putLenEncInt #-}++putWord24le :: Word32 -> Put+putWord24le v = do+    putWord16le $ fromIntegral v+    putWord8 $ fromIntegral (v `shiftR` 16)+{-# INLINE putWord24le #-}++getWord24le :: Get Word32+getWord24le = do+    a <- fromIntegral <$> getWord16le+    b <- fromIntegral <$> getWord8+    return $! a .|. (b `shiftL` 16)+{-# INLINE getWord24le #-}++putWord48le :: Word64 -> Put+putWord48le v = do+    putWord32le $ fromIntegral v+    putWord16le $ fromIntegral (v `shiftR` 32)+{-# INLINE putWord48le #-}++getWord48le :: Get Word64+getWord48le = do+    a <- fromIntegral <$> getWord32le+    b <- fromIntegral <$> getWord16le+    return $! a .|. (b `shiftL` 32)+{-# INLINE getWord48le #-}++getWord24be :: Get Word24+getWord24be = do+    a <- fromIntegral <$> getWord16be+    b <- fromIntegral <$> getWord8+    return $! b .|. (a `shiftL` 8)+{-# INLINE getWord24be #-}++getInt24be :: Get Int24+getInt24be = do+    a <- fromIntegral <$> getWord16be+    b <- fromIntegral <$> getWord8+    return $! fromIntegral $ (b .|. (a `shiftL` 8) :: Word24)+{-# INLINE getInt24be #-}++getWord40be, getWord48be, getWord56be :: Get Word64+getWord40be = do+    a <- fromIntegral <$> getWord32be+    b <- fromIntegral <$> getWord8+    return $! (a `shiftL` 8) .|. b+getWord48be = do+    a <- fromIntegral <$> getWord32be+    b <- fromIntegral <$> getWord16be+    return $! (a `shiftL` 16) .|. b+getWord56be = do+    a <- fromIntegral <$> getWord32be+    b <- fromIntegral <$> getWord24be+    return $! (a `shiftL` 24) .|. b+{-# INLINE getWord40be #-}+{-# INLINE getWord48be #-}+{-# INLINE getWord56be #-}++-- | Decode a fixed row/metadata structure with no trailing bytes. Compare the+-- consumed offset with the complete logical length: a parser may stop before+-- requesting a later lazy ByteString chunk, so its returned buffer alone cannot+-- establish full consumption. OK envelopes use their separate trailer parser.+getFromPacketExact :: Get a -> Packet -> IO a+getFromPacketExact parser (Packet lengthBytes _ body) = case parseDetailLazy parser body of+    Left (buffer,offset,message) -> throwIO (DecodePacketFailed buffer offset message)+    Right (_,offset,value)+        | offset == lengthBytes -> pure value+        | otherwise -> throwIO (DecodePacketFailed B.empty offset "trailing bytes in fixed protocol structure")+{-# INLINE getFromPacketExact #-}
+ src/Database/MySQL/Protocol/Progress.hs view
@@ -0,0 +1,49 @@+-- | MariaDB progress envelopes use a reserved ERR number without completing+-- the command. Parsing remains separate from callback/lifecycle policy.+module Database.MySQL.Protocol.Progress+    ( Progress(..), isProgressPacket, getProgress ) where++import Control.Monad (unless)+import Data.Binary.Parser+import Data.Bits+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as L+import Data.Word+import Database.MySQL.Protocol.Packet++-- | One server stage. 'progressThousandths' ranges from zero to 100000 (100%);+-- stages are one-based. Description bytes use the server encoding and are+-- copied out of the wire buffer. A report does not guarantee eventual success.+data Progress = Progress+    { progressStage :: !Word8+      -- ^ One-based current stage number, not overall completion.+    , progressStageCount :: !Word8+      -- ^ Server-reported number of stages.+    , progressThousandths :: !Word32+      -- ^ Current stage percentage multiplied by 1000 (100000 means 100%).+    , progressInfo :: !B.ByteString+      -- ^ Owned server description bytes; decode using the relevant charset.+    } deriving (Eq, Show)++-- | Recognize the reserved marker without flattening an ordinary packet.+isProgressPacket :: Packet -> Bool+isProgressPacket packet = L.take 3 (pBody packet) == L.pack [255,255,255]++-- | Parse the single-description wire format. Use an exact packet parser to+-- reject trailers; unknown description counts and invalid ranges fail closed.+getProgress :: Get Progress+getProgress = do+    marker <- getByteString 3+    unless (marker == B.pack [255,255,255]) (fail "invalid progress marker")+    strings <- getWord8+    unless (strings == 1) (fail "unsupported progress description count")+    stage <- getWord8+    stages <- getWord8+    a <- fromIntegral <$> getWord8+    b <- fromIntegral <$> getWord8+    c <- fromIntegral <$> getWord8+    let amount = a .|. (b `shiftL` 8) .|. (c `shiftL` 16)+    unless (stage > 0 && stage <= stages && amount <= 100000)+        (fail "invalid progress stage or percentage")+    info <- B.copy <$> getLenEncBytes+    pure (Progress stage stages amount info)
+ src/Database/MySQL/Query.hs view
@@ -0,0 +1,327 @@+{-# LANGUAGE BangPatterns #-}+-- | Session-aware MariaDB parameter rendering. The scanner identifies+-- placeholders, not a complete SQL grammar. Batch admission is deliberately+-- conservative because every statement is rendered before any is executed.+module Database.MySQL.Query where++import Control.Exception (Exception)+import Control.Monad+import Control.Arrow (first)+import Data.String (IsString(..))+import Data.Typeable+import Data.Char (toUpper, isAlphaNum)+import Data.List (intersperse, isSuffixOf)+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as BC+import qualified Data.ByteString.Lazy as L+import qualified Data.ByteString.Builder as BB+import Data.Binary.Put+import Database.MySQL.Protocol.MySQLValue++-- | SQL bytes in the connection's client encoding. String literals use UTF-8;+-- callers sending raw SQL under another charset must supply suitable bytes.+newtype Query = Query { fromQuery :: L.ByteString } deriving (Eq, Ord, Typeable)+instance Show Query where show = show . fromQuery+instance Read Query where readsPrec i = fmap (first Query) . readsPrec i+instance IsString Query where fromString = Query . BB.toLazyByteString . BB.stringUtf8++-- | A single value or a comma-separated expansion, for example inside IN (...).+-- An empty expansion renders NULL. Neither constructor supplies parentheses.+data Param = One MySQLValue | Many [MySQLValue]++-- | Session facts required to interpret templates and encode literals. Obtain+-- live snapshots under connection ownership; direct renderer callers must keep+-- their own snapshot current. MariaDB aliases charset/collation tracker entries,+-- so exact collation may be unknown after a charset change.+data RenderContext = RenderContext+    { renderSQLModes :: ![B.ByteString]+    , renderClientCharset :: !B.ByteString+    , renderConnectionCharset :: !B.ByteString+    , renderConnectionCollation :: !(Maybe B.ByteString)+    , renderServerVersion :: !Int+    } deriving (Show, Eq)++-- | UTF-8 fixture/offline context. This does not discover a live server's modes+-- or version and must not be used to repair an unknown connection context.+defaultRenderContext :: RenderContext+defaultRenderContext = RenderContext [] "utf8mb4" "utf8mb4" (Just "utf8mb4_unicode_ci") 0++-- | Rendering failures reported before any target command is written.+data ParameterError+    = InvalidParameterizedSQL String+    | UnsupportedRenderingContext String+    | UnencodableParameter String+    | ParameterCountMismatch+    | AmbiguousParameterizedBatch+    deriving (Show, Eq, Typeable)+instance Exception ParameterError+-- | Connected APIs preserve this arity exception for existing callers.+data WrongParamsCount = WrongParamsCount deriving (Show, Typeable)+instance Exception WrongParamsCount++-- | Trusted parameter encoders. An instance must produce one safe SQL value+-- (or a deliberate 'Many' expansion), respect the supplied context and return+-- validation errors through 'Left'. The driver cannot validate arbitrary SQL+-- emitted by a custom instance.+class QueryParam a where+    -- | Build a literal without performing I/O or changing the session.+    render :: RenderContext -> a -> Either ParameterError Put++instance QueryParam MySQLValue where+    render ctx (MySQLText value) = do+        bytes <- either (Left . UnencodableParameter) Right (encodeCharsetText (renderClientCharset ctx) value)+        pure (quoted ctx bytes)+    render _ (MySQLBytes value) = Right (binaryLiteral value)+    render _ (MySQLGeometry value) = Right (binaryLiteral value)+    render _ value = either (Left . UnencodableParameter) (const (Right (putTextField value))) (validateValue value)++instance QueryParam Param where+    render ctx (One value) = render ctx value+    render _ (Many []) = Right (putTextField MySQLNull)+    render ctx (Many values) = do+        puts <- traverse (render ctx) values+        pure (sequence_ (intersperse (putWord8 44) puts))++-- Hex literals preserve every byte independently of SQL mode/client charset.+binaryLiteral :: B.ByteString -> Put+binaryLiteral bytes = putByteString "X'" >> putBuilder (BB.byteStringHex bytes) >> putWord8 39++-- Quote doubling works in both backslash modes. Only backslash-enabled sessions+-- need the additional NUL/control/backslash escapes; ordinary runs are emitted+-- as ByteString slices to avoid allocating one builder per character.+quoted :: RenderContext -> B.ByteString -> Put+quoted ctx bytes = putWord8 39 >> chunks bytes >> putWord8 39+  where+    noBackslash = "NO_BACKSLASH_ESCAPES" `elem` renderSQLModes ctx+    special w = w==39 || (not noBackslash && w `elem` [0,10,13,26,92])+    chunks rest = do+        let (plain,tailBytes)=B.break special rest+        putByteString plain+        unless (B.null tailBytes) $ do+            let w=B.head tailBytes+            if w == 39+                then putByteString "''"+                else do+                    putWord8 92+                    putWord8 $ case w of+                        0 -> 48 -- NUL -> \0+                        10 -> 110 -- newline -> \n+                        13 -> 114 -- carriage return -> \r+                        26 -> 90 -- Ctrl-Z -> \Z+                        _ -> 92 -- backslash -> \\+            chunks (B.tail tailBytes)++-- | A per-template plan, reusable only with the context used for compilation.+-- Fragments preserve every original byte except actual placeholders. Safety+-- flags describe classification, not server acceptance or batch atomicity.+data QueryPlan = QueryPlan+    { planFragments :: ![L.ByteString]+    , planBatchSafe :: !Bool -- ^ No ambiguous parsing-context changes in a batch.+    , planOpaque :: !Bool -- ^ Execution may change facts tracking cannot establish.+    , planMultiple :: !Bool -- ^ More than one visible statement.+    , planTerminated :: !Bool -- ^ Last visible SQL token is a statement delimiter.+    } deriving Show++-- Allocation-light classification for short, flat ASCII SQL. Parentheses are+-- allowed only after a table target or a known built-in, so nested/qualified+-- user routine calls still take the conservative full-lexer path. Quoting,+-- block/hash comments and multi-statements take that path as well. A template+-- caller must also exclude '--' comments before interpreting question marks;+-- raw-state classification alone does not need to locate placeholders.+simpleStableQuery :: L.ByteString -> Bool+simpleStableQuery input = case L.toChunks input of+    [bytes] | B.length bytes<=512 -> simple bytes+    _ -> False+  where+    simple bytes = any (starts bytes) ["SELECT ","INSERT ","UPDATE ","DELETE ","REPLACE ","DO "]+        && B.all allowed bytes && scan bytes 0 False+    starts bytes word=B.length bytes>=B.length word && equalWord bytes 0 (B.length word) word+    allowed w = w>=48 && w<=57 || w>=65 && w<=90 || w>=97 && w<=122 || w `elem` [9,10,13,32,40,41,42,43,44,45,46,60,61,62,64,95]+    identifier w=w>=48 && w<=57 || w>=65 && w<=90 || w>=97 && w<=122 || w==95 || w==46 || w==36+    upper w=if w>=97 && w<=122 then w-32 else w+    equalWord bytes start end word = end-start==B.length word && loop 0+      where loop n=n==B.length word || upper (B.index bytes (start+n))==B.index word n && loop (n+1)+    scan bytes !i tableTarget+        | i>=B.length bytes=True+        | identifier (B.index bytes i)=+            let end=walk identifier i+                next=walk (\w -> w<=32) end+                wordIs=equalWord bytes i end+                call=next<B.length bytes && B.index bytes next==40+                known=any wordIs ["VALUES","VALUE","COUNT","SUM","AVG","MIN","MAX","COALESCE","IFNULL","ROW_COUNT","FOUND_ROWS","LAST_INSERT_ID","REPEAT","LENGTH","CAST","CONVERT"]+            in (not call || tableTarget || known) && scan bytes end (wordIs "INTO" || wordIs "INSERT" || wordIs "REPLACE")+        | otherwise=scan bytes (i+1) (if B.index bytes i<=32 then tableTarget else False)+      where+        walk predicate n | n<B.length bytes && predicate (B.index bytes n)=walk predicate (n+1)+                         | otherwise=n+{-# NOINLINE simpleStableQuery #-}++-- | Locate placeholders using MariaDB quoting, comments, modes and version+-- gates. Unknown routines and executable comments cannot establish batch safety.+compileQuery :: RenderContext -> Query -> Either ParameterError QueryPlan+compileQuery ctx (Query input)+  | fastTemplate = Right QueryPlan+        { planFragments = split (B.elemIndices 63 bytes) 0+        , planBatchSafe = True+        , planOpaque = False+        , planMultiple = False+        , planTerminated = False+        }+  | otherwise = do+    unless (renderClientCharset ctx `elem` ["utf8mb4","utf8mb3","utf8","ascii","latin1"]) (Left (UnsupportedRenderingContext "unsupported SQL template encoding"))+    holes <- scan 0 len []+    (visible,executable) <- visibleSQL 0 0 [] False+    let positions = reverse holes+        fragments = split positions 0+        code = BC.unpack visible+        tokens = tokenize code+        opaque = any (`elem` tokens) ["CALL","EXECUTE","PREPARE"] || any trackingControl tokens+        changes = opaque || any (`elem` tokens) ["SET","BEGIN","CREATE","ALTER","DROP"]+        -- A name followed by '(' can be a routine call or a table/type+        -- declaration. Unknown calls invalidate context and disallow batching;+        -- the allowlist below is an admission rule, not a full SQL parser.+        calls =+            [ name+            | (previous, name, "(") <- zip3 ("":tokens) tokens (drop 1 tokens)+            , previous `notElem`+                ["INTO", "INSERT", "REPLACE", "TABLE", "EXISTS", "KEY", "INDEX", "REFERENCES", "CONSTRAINT"]+            , case name of+                ch:_ -> isAlphaNum ch || ch == '_'+                [] -> False+            ]+        pureCalls =+            [ "VALUES", "VALUE", "IN", "NOT", "EXISTS", "AS", "SELECT"+            , "COUNT", "SUM", "AVG", "MIN", "MAX", "COALESCE", "IFNULL", "NULLIF", "IF"+            , "CAST", "CONVERT", "CONCAT", "CONCAT_WS", "ABS", "REPEAT", "LENGTH"+            , "OCTET_LENGTH", "CHAR_LENGTH", "CHARACTER_LENGTH", "HEX", "UNHEX"+            , "LOWER", "UPPER", "TRIM", "ROUND", "FLOOR", "CEIL", "MOD", "SLEEP"+            , "ROW_COUNT", "FOUND_ROWS", "LAST_INSERT_ID", "CONNECTION_ID", "VERSION", "DATABASE"+            , "NOW", "CURRENT_TIMESTAMP", "JSON_OBJECT", "JSON_ARRAY"+            , "INT", "INTEGER", "TINYINT", "SMALLINT", "MEDIUMINT", "BIGINT", "DECIMAL", "NUMERIC"+            , "FLOAT", "DOUBLE", "BIT", "CHAR", "VARCHAR", "BINARY", "VARBINARY", "BLOB", "TEXT"+            , "TINYBLOB", "MEDIUMBLOB", "LONGBLOB", "TINYTEXT", "MEDIUMTEXT", "LONGTEXT"+            , "TIME", "DATETIME", "TIMESTAMP", "YEAR", "ENUM", "SET"+            ]+        unknownCalls = any (`notElem` pureCalls) calls+        statements = filter (not . null) (map words (splitStatements code))+        safe = not changes && not executable && not unknownCalls && all safeHead statements+    -- Active executable comments can contain complete statements, including+    -- tracking-control changes hidden from visibleSQL. Their effects are opaque+    -- even when the visible outer statement looks like a stable SELECT.+    pure QueryPlan+        { planFragments = fragments+        , planBatchSafe = safe+        , planOpaque = opaque || unknownCalls || executable+        , planMultiple = length statements > 1+        , planTerminated = case B.unsnoc (B.dropWhileEnd (<=32) visible) of+            Just (_, byte) -> byte == 59+            Nothing -> False+        }+  where+    -- No quoting/comments/multiple statements can enter this path. The same+    -- full-byte/call classification as raw SQL proves context stability; every+    -- question mark is therefore a placeholder. Complex templates retain the+    -- complete mode/version-aware lexer below.+    fastTemplate = renderClientCharset ctx `elem` ["utf8mb4","utf8mb3","utf8","ascii","latin1"]+        && B.length bytes<=512 && not ("--" `B.isInfixOf` bytes)+        && simpleStableQuery (L.fromStrict (B.map (\w -> if w==63 then 32 else w) bytes))+    trackingControl token = any (\name -> token==name || ("."++name) `isSuffixOf` token) ["SESSION_TRACK_SYSTEM_VARIABLES","SESSION_TRACK_STATE_CHANGE"]+    bytes = L.toStrict input+    len = B.length bytes+    at = B.index bytes+    has i x = i + B.length x <= len && x `B.isPrefixOf` B.drop i bytes+    noBackslash = "NO_BACKSLASH_ESCAPES" `elem` renderSQLModes ctx+    ansi = "ANSI_QUOTES" `elem` renderSQLModes ctx+    err = Left . InvalidParameterizedSQL+    split [] start = [L.fromStrict (B.drop start bytes)]+    split (i:is) start = L.fromStrict (B.take (i-start) (B.drop start bytes)) : split is (i+1)+    scan !i end acc+        | i >= end = Right acc+        | at i == 63 = scan (i+1) end (i:acc)+        | at i `elem` [39,34,96] = quote (at i) (i+1) end >>= \j -> scan j end acc+        | at i == 35 = scan (lineEnd (i+1) end) end acc+        | has i "--" && (i+2 == end || at (i+2) <= 32 || at (i+2)==127) = scan (lineEnd (i+2) end) end acc+        | has i "/*" = do+            (begin, active, nested) <- commentBody i+            close <- blockEnd nested (i+2) end+            acc' <- if active then scan begin close acc else Right acc+            scan (close+2) end acc'+        | otherwise = scan (i+1) end acc+    quote delimiter i end+        | i >= end = err "unterminated quoted token"+        | at i == delimiter = if i+1<end && at (i+1)==delimiter then quote delimiter (i+2) end else Right (i+1)+        | at i==92 && delimiter/=96 && (delimiter/=34 || not ansi) && not noBackslash = if i+1<end then quote delimiter (i+2) end else err "unfinished escape"+        | otherwise = quote delimiter (i+1) end+    lineEnd i end | i>=end || at i==10 || at i==13 = i+                  | otherwise = lineEnd (i+1) end+    -- MariaDB consumes exactly five or six version digits. Fewer digits belong+    -- to ordinary executable SQL; any seventh digit remains in that SQL too.+    -- Only skipped version comments tolerate one nested ordinary comment.+    -- Keep placeholder scanning and effect classification on the same rule.+    commentBody i+        | not executable = Right (i+2, False, False)+        | has begin "!" = Left (UnsupportedRenderingContext "reversed executable comments are not supported by the parameter renderer")+        | otherwise =+            let digits = B.takeWhile (\w -> w >= 48 && w <= 57) (B.take 6 (B.drop begin bytes))+                versioned = B.length digits >= 5+                gate = B.foldl' (\value digit -> value * 10 + fromIntegral (digit-48)) 0 digits+                active = not versioned || gate <= renderServerVersion ctx && (maria || gate < 50700 || gate > 99999)+            in Right (begin + if versioned then B.length digits else 0, active, versioned && not active)+      where+        maria = has i "/*M!"+        executable = maria || has i "/*!"+        begin = i + if maria then 4 else 3+    blockEnd nested i end+        | i+1 >= end = err "unterminated block comment"+        | has i "*/" = Right i+        | nested && has i "/*" = do+            inner <- blockEnd False (i+2) end+            blockEnd nested (inner+2) end+        | otherwise = blockEnd nested (i+1) end+    visibleSQL i start chunks executable+        | i >= len = Right (B.concat (reverse (B.drop start bytes:chunks)),executable)+        | at i `elem` [39,34,96] = do+            j <- quote (at i) (i+1) len+            let identifier=at i==96 || at i==34 && ansi+                rawName=BC.map toUpper (B.take (j-i-2) (B.drop (i+1) bytes))+                replacement=if identifier then+                    if rawName `elem` ["SESSION_TRACK_SYSTEM_VARIABLES","SESSION_TRACK_STATE_CHANGE"] then rawName else "__QUOTED_IDENTIFIER__"+                    else " "+            visibleSQL j j (replacement:B.take (i-start) (B.drop start bytes):chunks) executable+        | at i==35 || (has i "--" && (i+2==len || at (i+2)<=32 || at (i+2)==127)) = do+            let j=lineEnd (i+1) len+            visibleSQL j j (" ":B.take (i-start) (B.drop start bytes):chunks) executable+        | has i "/*" = do+            (_, active, nested) <- commentBody i+            close <- blockEnd nested (i+2) len+            let j=close+2+            visibleSQL j j (" ":B.take (i-start) (B.drop start bytes):chunks) (executable || active)+        | otherwise = visibleSQL (i+1) start chunks executable+    tokenize [] = []+    tokenize (ch:rest)+        | isAlphaNum ch || ch `elem` ("_$." :: String) = let (word,tail')=span (\x -> isAlphaNum x || x `elem` ("_$." :: String)) rest in map toUpper (ch:word):tokenize tail'+        | ch `elem` ("(),;" :: String) = [ch]:tokenize rest+        | otherwise = tokenize rest+    splitStatements str = case break (==';') str of (a,[]) -> [a];(a,_:rest)->a:splitStatements rest+    safeHead (x:_) = map toUpper x `elem` ["INSERT","UPDATE","DELETE","REPLACE","SELECT","DO"]+    safeHead [] = True++-- | Render a compiled plan with the same context and exactly one value per hole.+renderPlan :: QueryParam p => RenderContext -> QueryPlan -> [p] -> Either ParameterError Query+renderPlan ctx plan values = Query . runPut <$> renderPlanPut ctx plan values++-- | Builder form used to join a batch without encoding/copying every query twice.+renderPlanPut :: QueryParam p => RenderContext -> QueryPlan -> [p] -> Either ParameterError Put+renderPlanPut ctx plan values = fill (planFragments plan) values+  where+    fill [x] [] = Right (putLazyByteString x)+    fill (x:xs) (v:vs) = do+        value <- render ctx v+        rest <- fill xs vs+        pure (putLazyByteString x >> value >> rest)+    fill _ _ = Left ParameterCountMismatch++-- | Compile and render with explicitly supplied session facts. Connected APIs+-- acquire these facts under ownership; pure callers must establish them first.+renderParams :: QueryParam p => RenderContext -> Query -> [p] -> Either ParameterError Query+renderParams ctx qry values = compileQuery ctx qry >>= \plan -> renderPlan ctx plan values
+ src/Database/MySQL/Session.hs view
@@ -0,0 +1,216 @@+-- | Bounded session observations carried by MariaDB OK packets. This module+-- owns retained bytes; callers never keep slices of a large receive buffer.+module Database.MySQL.Session where++import Control.Monad (foldM, unless, when)+import Data.Bits ((.&.))+import qualified Data.ByteString as B+import Data.ByteString (ByteString)+import qualified Data.Map.Strict as M+import Data.Word+import qualified Data.ByteString.Char8 as BC++-- | One server-reported change. Unknown kinds retain their length-delimited+-- payload rather than guessing a layout. Values can contain application data;+-- do not log session observations indiscriminately.+data SessionChange+    = SystemVariableChanged !ByteString !ByteString -- ^ Variable name and value.+    | SchemaChanged !ByteString -- ^ Empty bytes mean no selected database.+    | SessionStateChanged !Bool -- ^ The server's generic state-change marker.+    | TransactionCharacteristicsChanged !ByteString -- ^ Server-provided SQL text.+    | TransactionStateChanged !ByteString -- ^ Eight-byte MariaDB transaction state.+    | UnknownSessionChange !Word8 !ByteString -- ^ Unimplemented tracker kind and payload.+    deriving (Eq, Show)++-- | Current observations, not a complete inventory of server settings. 'Nothing'+-- means unknown; an absent variable has not been observed or is no longer tracked.+-- A snapshot reflects only replies consumed so far, including during streaming.+-- 'sessionLastChanges' is the latest tracking envelope, not an event queue: use+-- 'sessionChangeNumber' to recognize replacement, and snapshot fields for final+-- state after multiple results. Transaction flags also work without tracking.+data SessionState = SessionState+    { sessionSchema :: !(Maybe ByteString) -- ^ Known schema; Just empty means none.+    , sessionVariables :: !(M.Map ByteString ByteString) -- ^ Observed tracked variables.+    , sessionAutocommit :: !(Maybe Bool) -- ^ SERVER_STATUS_AUTOCOMMIT, not transaction activity.+    , sessionInTransaction :: !(Maybe Bool) -- ^ SERVER_STATUS_IN_TRANS.+    , sessionTransactionCharacteristics :: !(Maybe ByteString) -- ^ When the server tracks them.+    , sessionTransactionState :: !(Maybe ByteString) -- ^ When the server tracks it.+    , sessionLastChanges :: ![SessionChange] -- ^ Latest bounded tracker envelope, in wire order.+    , sessionChangeNumber :: !Word64 -- ^ Advances on a tracker envelope or invalidation.+    } deriving (Eq, Show)++-- Internal accounting avoids rescanning every tracked variable per update.+data SessionCache = SessionCache+    { sessionObservation :: !SessionState+    , sessionVariableBytes :: !Word64+    }++emptySessionCache :: Word64 -> SessionCache+emptySessionCache serial = SessionCache+    (SessionState Nothing M.empty Nothing Nothing Nothing Nothing [] serial) 0++-- The caller has already bounded the complete OK envelope. Additionally bound+-- item counts, even for zero-length payloads, and reject known malformed kinds.+decodeSessionChanges :: ByteString -> Either String [SessionChange]+decodeSessionChanges = items 0 []+  where+    items :: Int -> [SessionChange] -> ByteString -> Either String [SessionChange]+    items count acc bytes+        | B.null bytes = Right (reverse acc)+        | count >= 65535 = Left "too many session tracking items"+        | otherwise = do+            (payload, rest) <- lengthString (B.tail bytes)+            (next, changes) <- case B.head bytes of+                0 -> variables (count + 1) acc payload+                kind -> do+                    change <- case kind of+                        1 -> SchemaChanged <$> single payload+                        -- This tracker alone has a raw marker after the outer+                        -- length. There is no redundant inner string length.+                        2 -> case payload of+                            "0" -> pure (SessionStateChanged False)+                            "1" -> pure (SessionStateChanged True)+                            _ -> Left "invalid session state-change marker"+                        4 -> TransactionCharacteristicsChanged <$> single payload+                        5 -> do+                            value <- single payload+                            unless (B.length value == 8) (Left "invalid transaction state length")+                            pure (TransactionStateChanged value)+                        _ -> pure (UnknownSessionChange kind (B.copy payload))+                    pure (count + 1, change : acc)+            items next changes rest+    variables count acc bytes+        | B.null bytes = Right (count, acc)+        | count >= 65535 = Left "too many tracked variables"+        | otherwise = do+            (name, rest) <- lengthString bytes+            (value, remaining) <- lengthString rest+            when (B.null name) (Left "empty tracked variable name")+            variables (count + 1) (SystemVariableChanged (B.copy name) (B.copy value) : acc) remaining+    single payload = do+        (value, rest) <- lengthString payload+        unless (B.null rest) (Left "trailing session tracking payload")+        pure (B.copy value)++-- Non-NULL length-encoded strings, with arithmetic in Word64 before conversion.+lengthString :: ByteString -> Either String (ByteString, ByteString)+lengthString bytes = do+    when (B.null bytes) (Left "truncated length string")+    let tag = B.head bytes+        after = B.tail bytes+        takeLength width = do+            when (B.length after < width) (Left "truncated encoded length")+            let size = B.foldr (\byte acc -> acc * 256 + fromIntegral byte)+                            (0 :: Word64) (B.take width after)+            pure (size, B.drop width after)+    (count, rest) <- case tag of+        0xfc -> takeLength 2+        0xfd -> takeLength 3+        0xfe -> takeLength 8+        _ | tag < 0xfb -> Right (fromIntegral tag, after)+          | otherwise -> Left "invalid non-null string length"+    when (count > fromIntegral (B.length rest)) (Left "truncated length string body")+    pure (B.splitAt (fromIntegral count) rest)++applySessionStatus :: Word16 -> SessionCache -> SessionCache+applySessionStatus flags cache = cache {sessionObservation = state+    { sessionAutocommit = Just (flags .&. 2 /= 0)+    , sessionInTransaction = Just (flags .&. 1 /= 0)+    }}+  where state = sessionObservation cache++-- Replace the latest envelope, then apply deltas with incremental byte charges.+-- Both accumulated variables and the retained latest envelope share the budget.+applySessionChanges :: Word64 -> [SessionChange] -> SessionCache -> Either String SessionCache+applySessionChanges = applySessionObservations TrackingEnvelope++-- Helper SELECT/configuration observations update facts without creating a+-- tracker event. Charge the envelope actually retained, including an older one.+applySessionFacts :: Word64 -> [SessionChange] -> SessionCache -> Either String SessionCache+applySessionFacts = applySessionObservations SnapshotFacts++data ObservationSource = TrackingEnvelope | SnapshotFacts++applySessionObservations :: ObservationSource -> Word64 -> [SessionChange] -> SessionCache -> Either String SessionCache+applySessionObservations source limit changes cache = do+    -- MariaDB keys trackers by variable storage offset, so charset and+    -- collation aliases may report only one name. Neither value determines+    -- the other in general. Forget an unreported alias before applying the+    -- batch, releasing its charge even when the replacement is near the limit.+    -- Explicit deltas for both names remain valid in either wire order.+    let charsetReported = hasVariableChange "character_set_connection" changes+        collationReported = hasVariableChange "collation_connection" changes+        coherent = case (charsetReported, collationReported) of+            (True, False) -> forgetVariable "collation_connection" cache+            (False, True) -> forgetVariable "character_set_connection" cache+            _ -> cache+    applied <- foldM apply coherent changes+    -- A changed subscription invalidates values the server will no longer+    -- report. Recount only on this cold configuration path, never per query.+    let updated = case reverse [value | SystemVariableChanged "session_track_system_variables" value <- changes] of+            selected : _ | selected /= "*" ->+                let names = BC.split ',' selected+                    observed = sessionObservation applied+                    kept = M.filterWithKey (\name _ -> name `elem` names || name == "session_track_system_variables")+                               (sessionVariables observed)+                in applied {sessionObservation = observed {sessionVariables = kept},+                    sessionVariableBytes = sum [bytes key + bytes value | (key,value) <- M.toList kept]}+            _ -> applied+        state = sessionObservation updated+        prior = sessionObservation cache+        envelope = case source of TrackingEnvelope -> changes; SnapshotFacts -> sessionLastChanges prior+        serial = case source of TrackingEnvelope -> sessionChangeNumber prior + 1; SnapshotFacts -> sessionChangeNumber prior+        retained = sessionVariableBytes updated + sum (map changeBytes envelope)+            + maybeBytes (sessionSchema state)+            + maybeBytes (sessionTransactionCharacteristics state)+            + maybeBytes (sessionTransactionState state)+    when (retained > limit) (Left "retained session state exceeds metadata budget")+    pure updated {sessionObservation = state+        {sessionLastChanges = envelope, sessionChangeNumber = serial}}+  where+    apply current change = case change of+        SystemVariableChanged name value -> do+            let state = sessionObservation current+                variables = sessionVariables state+                old = maybe 0 (\prior -> bytes name + bytes prior) (M.lookup name variables)+                charge = sessionVariableBytes current - old + bytes name + bytes value+            when (charge > limit || M.size variables >= 65535 && M.notMember name variables)+                (Left "tracked variables exceed session budget")+            pure current {sessionVariableBytes = charge, sessionObservation = state+                {sessionVariables = M.insert name value variables,+                 sessionSchema = if name == "session_track_schema" && value `elem` ["0","OFF"]+                    then Nothing else sessionSchema state,+                 sessionTransactionState = if name == "session_track_transaction_info" && value == "OFF"+                    then Nothing else sessionTransactionState state,+                 sessionTransactionCharacteristics = if name == "session_track_transaction_info" && value /= "CHARACTERISTICS"+                    then Nothing else sessionTransactionCharacteristics state}}+        SchemaChanged value -> pure (modifyState (\s -> s {sessionSchema = Just value}) current)+        TransactionCharacteristicsChanged value -> pure (modifyState+            (\s -> s {sessionTransactionCharacteristics = Just value}) current)+        TransactionStateChanged value -> pure (modifyState+            (\s -> s {sessionTransactionState = Just value}) current)+        _ -> pure current+    forgetVariable name current = case M.lookup name (sessionVariables (sessionObservation current)) of+        Nothing -> current+        Just value -> current+            { sessionVariableBytes = sessionVariableBytes current - bytes name - bytes value+            , sessionObservation = (sessionObservation current)+                {sessionVariables = M.delete name (sessionVariables (sessionObservation current))}+            }+    modifyState change current = current {sessionObservation = change (sessionObservation current)}+    maybeBytes = maybe 0 bytes+    bytes = fromIntegral . B.length+    changeBytes change = case change of+        SystemVariableChanged name value -> bytes name + bytes value+        SchemaChanged value -> bytes value+        SessionStateChanged _ -> 1+        TransactionCharacteristicsChanged value -> bytes value+        TransactionStateChanged value -> bytes value+        UnknownSessionChange _ value -> bytes value++-- Inspect only tracking envelopes, never ordinary rows/replies without changes.+hasVariableChange :: ByteString -> [SessionChange] -> Bool+hasVariableChange name = any matches+  where+    matches (SystemVariableChanged changed _) = name == changed+    matches _ = False
+ src/Database/MySQL/TLS.hs view
@@ -0,0 +1,104 @@+{-|+Module      : Database.MySQL.TLS+Description : MariaDB TLS connections via the @tls@ package+Copyright   : (c) Winterland, 2016+License     : BSD+Maintainer  : Anton Antich <anton.antich@gmail.com>+Stability   : experimental+Portability : macOS and Linux tested++TLS connections use the same ownership, limits and session initialization as+TCP. Supply certificate/hostname validation through t'Network.TLS.ClientParams'.++Use 'makeClientParams' with 'SystemCAStore' or a managed 'CustomCAStore', then+reuse the resulting parameters in the connection factory. The tuple's name is+the server certificate DNS identity (and default SNI), which can differ from+the address in 'ciHost'. Do not disable verification to work around CA/name+errors. @makeClientParams'@ also provides a client certificate for X.509 accounts.++@MozillaCAStore@ uses an installed static snapshot, not automatically updated+OS roots; see the packaged @docs/CA_BUNDLE.md@ for provenance and update policy.+TLS protects transport; PAM/GSSAPI options do not replace peer verification. See+the <https://github.com/aantich/mariadb-haskell/blob/main/docs/USER_GUIDE.md connection guide>.++-}++module Database.MySQL.TLS (+      connect+    , connectDetail+    , connectWithOptions+    , connectWithAuthentication+    , connectDetailWithAuthentication+    , connectDetailWithOptions+    , module Data.TLSSetting+    ) where++import           Control.Exception              (bracketOnError, throwIO, finally)+import qualified Data.Binary                    as Binary+import qualified Data.Binary.Put                as Binary+import qualified Data.Connection                as Conn+import           Data.IORef                     (writeIORef)+import           Data.Bits                      (testBit)+import           Control.Monad                  (when)+import           Data.TLSSetting+import           Database.MySQL.Authentication+import           Database.MySQL.Connection      hiding (connect, connectDetail, connectWithOptions, connectDetailWithOptions, connectWithAuthentication, connectDetailWithAuthentication)+import           Database.MySQL.Protocol.Auth+import           Database.MySQL.Protocol.Packet+import qualified Network.TLS                    as TLS+import qualified System.IO.Streams.TCP          as TCP+import qualified System.IO.Streams.TLS          as TLS++--------------------------------------------------------------------------------++-- | Provide a t'Network.TLS.ClientParams' and a subject name to establish a TLS connection.+--+connect :: ConnectInfo -> (TLS.ClientParams,String) -> IO MySQLConn+connect = connectWithOptions defaultConnectionOptions+-- | Connect over TLS with explicit resource/session options. Failure during+-- handshake, authentication or initialization closes both TLS and raw transport.+connectWithOptions :: ConnectionOptions -> ConnectInfo -> (TLS.ClientParams,String) -> IO MySQLConn+connectWithOptions options ci params = snd <$> connectDetailWithOptions options ci params+-- | Establish TLS and return the MariaDB greeting alongside the connection.+connectDetail :: ConnectInfo -> (TLS.ClientParams,String) -> IO (Greeting,MySQLConn)+connectDetail = connectDetailWithOptions defaultConnectionOptions+-- | Explicit-options variant of 'connectDetail'. The plaintext greeting uses+-- sequence zero, SSLRequest one and the authenticated TLS response sequence two.+connectDetailWithOptions :: ConnectionOptions -> ConnectInfo -> (TLS.ClientParams,String) -> IO (Greeting,MySQLConn)+connectDetailWithOptions options = connectDetailWithAuthentication options defaultAuthenticationOptions++-- | TLS connection with explicit PAM callbacks or a pinned GSSAPI principal.+-- Certificate verification remains the responsibility of the supplied TLS+-- parameters; external authentication does not replace peer verification.+connectWithAuthentication :: ConnectionOptions -> AuthenticationOptions -> ConnectInfo -> (TLS.ClientParams,String) -> IO MySQLConn+connectWithAuthentication options auth ci params = snd <$> connectDetailWithAuthentication options auth ci params++-- | Greeting-returning variant of 'connectWithAuthentication'.+connectDetailWithAuthentication :: ConnectionOptions -> AuthenticationOptions -> ConnectInfo -> (TLS.ClientParams,String) -> IO (Greeting,MySQLConn)+connectDetailWithAuthentication options auth ci (params,name) = withConnectDeadline options $ do+    validateOptions options+    bracketOnError (TCP.connectSocketTimeout (connectTimeoutMicros options) (ciHost ci) (ciPort ci) >>= TCP.socketToConnectionAdaptive bUFSIZE) Conn.close $ \raw -> do+        input <- decodeInputStreamWithLimit (authenticationWireLimit options) (Conn.source raw)+        packet <- readPacket input+        if pSeqN packet /= 0 then throwIO (ProtocolError "invalid greeting sequence") else pure ()+        if isERR packet then authenticationDiagnostic (decodeFromPacket packet) >>= throwIO . ERRException else pure ()+        greeting <- authenticationDiagnostic (getFromPacketExact getGreeting packet)+        if not (supportTLS (greetingCaps greeting)) then throwIO (AuthException "MariaDB server does not offer TLS") else pure ()+        let caps=negotiatedCapabilities options greeting+            params'=params {TLS.clientServerIdentification=(name,"")}+            (sock,addr)=Conn.connExtraInfo raw+            ssl=SSLRequest (caps + 0x800) (fromIntegral (maximumMessageBytes options)) (ciCharset ci)+        Conn.send raw (Binary.runPut (Binary.put (encodeToPacket 1 ssl)))+        bracketOnError (TLS.contextNew sock params') (\ctx -> TLS.contextClose ctx `finally` Conn.close raw) $ \ctx -> do+            TLS.handshake ctx+            secure <- TLS.tLsToConnection (ctx,addr)+            (authConn,conn) <- newAuthenticatingTransportConnection options (Conn.source secure) (Conn.send secure) (TLS.contextClose ctx `finally` Conn.close raw)+            setExchangeSequence conn 2+            writeIORef (connectionCapabilities conn) (caps+0x800)+            mysqlWrite conn (encodeToPacket 2 (mkAuthForAuthentication auth options (caps+0x800) ci greeting))+            response <- authenticationDiagnostic (readConnPacket authConn)+            completeAuthWithOptions auth authConn greeting True (ciPassword ci) response+            when (testBit caps 5) (connectionCompressionStart conn)+            role <- initializeSession conn+            rememberInitialSession conn ci role+            pure (greeting,conn)
+ src/System/IO/Streams/TCP.hs view
@@ -0,0 +1,196 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE CPP #-}++-- | This module provides convenience functions for interfacing raw tcp.+--+-- Please use 'E.bracket' or its friends to ensure exception safety.+--+-- This module is intended to be imported @qualified@, e.g.:+--+-- @+-- import           "Data.Connection"+-- import qualified "System.IO.Streams.TCP" as TCP+-- @+--+module System.IO.Streams.TCP+  ( TCPConnection+    -- * client+  , connect+  , connectSocket+  , connectSocketTimeout+  , socketToConnection+  , socketToConnectionAdaptive+  , defaultChunkSize+    -- * server+  , bindAndListen+  , bindAndListenWith+  , accept+  , acceptWith+  ) where++import qualified Control.Exception         as E+import           System.Timeout (timeout)+import           Control.Monad+import           Data.Connection+import qualified Data.ByteString           as B+import qualified Data.ByteString.Lazy.Internal as L+import qualified Network.Socket            as N+import qualified Network.Socket.ByteString as NB+import qualified Network.Socket.ByteString.Lazy as NL+import qualified System.IO.Streams         as S+import           Foreign.Storable   (sizeOf)+import           Data.IORef (newIORef,readIORef,writeIORef)++addrAny :: N.HostAddress+#if MIN_VERSION_network(2,7,0)+addrAny = N.tupleToHostAddress (0,0,0,0)+#else+addrAny = N.iNADDR_ANY+#endif++-- | Type alias for tcp connection.+--+-- Normally you shouldn't use 'N.Socket' in 'connExtraInfo' directly, this field is+-- intend for used with 'N.setSocketOption' if you need to.+--+type TCPConnection = Connection (N.Socket, N.SockAddr)++-- | The chunk size used for I\/O, less the memory management overhead.+--+-- Currently set to 32k.+--+defaultChunkSize :: Int+defaultChunkSize = 32 * k - chunkOverhead+  where+    k = 1024+    chunkOverhead = 2 * sizeOf (undefined :: Int)++-- | Open a raw TCP socket for the address and port. Name resolution uses+-- @AI_ADDRCONFIG@ and @AI_NUMERICSERV@; usable IPv4/IPv6 addresses are tried+-- within one ten-second default deadline. @TCP_NODELAY@ is enabled.+-- The caller owns the returned socket. This does not authenticate MariaDB.+--+connectSocket :: N.HostName             -- ^ hostname to connect to+              -> N.PortNumber           -- ^ port number to connect to+              -> IO (N.Socket, N.SockAddr)+connectSocket = connectSocketTimeout 10000000++-- | Resolve and try all addresses within one deadline, including DNS time.+-- Each failed attempt closes its socket. Only IO errors permit fallback;+-- asynchronous cancellation propagates immediately. No authentication or TLS+-- failure is retried against another address after this function returns.+connectSocketTimeout :: Int -> N.HostName -> N.PortNumber -> IO (N.Socket, N.SockAddr)+connectSocketTimeout deadline host port+    | deadline <= 0 = ioError (userError "connection timeout must be positive")+    | otherwise = timeout deadline establish >>= maybe+        (ioError (userError "TCP connection establishment timed out")) pure+  where+    establish = N.getAddrInfo (Just hints) (Just host) (Just (show port)) >>= attempts+    hints = N.defaultHints+        { N.addrFlags = [N.AI_ADDRCONFIG, N.AI_NUMERICSERV]+        , N.addrSocketType = N.Stream+        }+    attempts [] = ioError (userError "hostname resolved to no usable addresses")+    attempts (address:rest) = do+        result <- E.try $ E.bracketOnError+            (N.socket (N.addrFamily address) (N.addrSocketType address) (N.addrProtocol address))+            N.close $ \sock -> do+                N.connect sock (N.addrAddress address)+                N.setSocketOption sock N.NoDelay 1+                pure (sock, N.addrAddress address)+        case result of+            Right connected -> pure connected+            Left (err :: E.IOException) -> if null rest then E.throwIO err else attempts rest++-- | Make a t'Connection' from a 'Network.Socket.Socket' with given buffer size.+--+socketToConnection+    :: Int                      -- ^ receive buffer size+    -> (N.Socket, N.SockAddr)   -- ^ socket address pair+    -> IO TCPConnection+socketToConnection bufsiz (sock, addr) = do+    is <- S.makeInputStream $ do+        s <- NB.recv sock bufsiz+        return $! if B.null s then Nothing else Just s+    return (Connection is (sendSocket sock) (N.close sock) (sock, addr))++-- | Start with a small receive allocation and grow on full reads, up to the+-- supplied ceiling. Each recv still returns independent immutable bytes; no+-- mutable buffer is reused underneath a returned row or metadata slice.+socketToConnectionAdaptive :: Int -> (N.Socket,N.SockAddr) -> IO TCPConnection+socketToConnectionAdaptive ceilingBytes (sock,addr) = do+    when (ceilingBytes<=0) (E.throwIO (userError "receive buffer ceiling must be positive"))+    capacity <- newIORef (min 4096 ceilingBytes)+    is <- S.makeInputStream $ do+        size <- readIORef capacity+        bytes <- NB.recv sock size+        when (B.length bytes==size && size<ceilingBytes) $+            writeIORef capacity (size+min size (ceilingBytes-size))+        pure $! if B.null bytes then Nothing else Just bytes+    pure (Connection is (sendSocket sock) (N.close sock) (sock,addr))++sendSocket :: N.Socket -> L.ByteString -> IO ()+sendSocket _ L.Empty = pure ()+sendSocket sock (L.Chunk bytes L.Empty) = unless (B.null bytes) (NB.sendAll sock bytes)+sendSocket sock bytes = NL.sendAll sock bytes++-- | Connect to server using 'defaultChunkSize'.+--+connect :: N.HostName             -- ^ hostname to connect to+        -> N.PortNumber           -- ^ port number to connect to+        -> IO TCPConnection+connect host port = connectSocket host port >>= socketToConnection defaultChunkSize++-- | Bind and listen on port with a limit on connection count.+--+-- This function will set @SO_REUSEADDR@, @TCP_NODELAY@ before binding.+--+bindAndListen :: Int                 -- ^ connection limit+              -> N.PortNumber        -- ^ port number+              -> IO N.Socket+bindAndListen = bindAndListenWith $ \ sock -> do+    N.setSocketOption sock N.ReuseAddr 1+    N.setSocketOption sock N.NoDelay 1++-- | Bind and listen on port with a limit on connection count.+--+-- Note: The following socket options are inherited by a connected TCP socket from the listening socket:+--+-- @+-- SO_DEBUG+-- SO_DONTROUTE+-- SO_KEEPALIVE+-- SO_LINGER+-- SO_OOBINLINE+-- SO_RCVBUF+-- SO_RCVLOWAT+-- SO_SNDBUF+-- SO_SNDLOWAT+-- TCP_MAXSEG+-- TCP_NODELAY+-- @+--+bindAndListenWith :: (N.Socket -> IO ()) -- ^ set socket options before binding+                  -> Int                 -- ^ connection limit+                  -> N.PortNumber        -- ^ port number+                  -> IO N.Socket+bindAndListenWith f maxc port =+    E.bracketOnError (N.socket N.AF_INET N.Stream 0)+                     N.close+                     (\sock -> do f sock+                                  N.bind sock (N.SockAddrInet port addrAny)+                                  N.listen sock maxc+                                  return sock+                     )++-- | Accept a connection with 'defaultChunkSize'.+--+accept :: N.Socket -> IO TCPConnection+accept = acceptWith (socketToConnection defaultChunkSize)++-- | Accept a connection with user customization.+--+acceptWith :: ((N.Socket, N.SockAddr) -> IO TCPConnection) -- ^ set socket options, adjust receive buffer, etc.+           -> N.Socket+           -> IO TCPConnection+acceptWith f = f <=< N.accept
+ src/System/IO/Streams/TLS.hs view
@@ -0,0 +1,107 @@+{-# LANGUAGE ScopedTypeVariables #-}++-- | Low-level raw TLS transport helpers. These open TLS immediately on the+-- socket; MariaDB instead requires a plaintext greeting and SSLRequest first.+-- MariaDB applications must use "Database.MySQL.TLS" for that handshake and+-- its connection deadlines/ownership policy. Bracket raw transports yourself.+--+-- This module is intended to be imported @qualified@, e.g.:+--+-- @+-- import           "Data.Connection"+-- import qualified "System.IO.Streams.TLS" as TLS+-- @+--+module System.IO.Streams.TLS+  ( TLSConnection+    -- * client+  , connect+  , connectTLS+  , tLsToConnection+    -- * server+  , accept+    -- * re-export+  , module Data.TLSSetting+  ) where++import qualified Control.Exception     as E+import           Data.Connection+import qualified Data.ByteString       as B+import qualified Data.ByteString.Char8 as BC+import           Data.TLSSetting+import qualified Network.Socket        as N+import           Network.TLS           (ClientParams, Context, ServerParams)+import qualified Network.TLS           as TLS+import qualified System.IO.Streams     as Stream+import qualified System.IO.Streams.TCP as TCP+++-- | Type alias for tls connection.+--+-- Normally you shouldn't use 'TLS.Context' in 'connExtraInfo' directly.+--+type TLSConnection = Connection (TLS.Context, N.SockAddr)++-- | Make a t'Connection' from a 'Context'.+--+tLsToConnection :: (Context, N.SockAddr)    -- ^ TLS connection / socket address pair+                -> IO TLSConnection+tLsToConnection (ctx, addr) = do+    is <- Stream.makeInputStream input+    return (Connection is write (closeTLS ctx) (ctx, addr))+  where+    -- Preserve TLS failures and asynchronous cancellation. Treating either as+    -- EOF would let the caller mistake an interrupted reply for a clean close.+    input = do+        s <- TLS.recvData ctx+        return $! if B.null s then Nothing else Just s+    write s = TLS.sendData ctx s++-- | Close a TLS 'Context' and its underlying socket.+--+closeTLS :: Context -> IO ()+closeTLS ctx = (TLS.bye ctx `E.finally` TLS.contextClose ctx) -- sometimes socket was closed before 'TLS.bye'+    `E.catch` (\(_::E.SomeException) -> return ())   -- so we catch the 'Broken pipe' error here++-- | Convenience function for initiating an TLS connection to the given+-- @('Network.Socket.HostName', 'Network.Socket.PortNumber')@ combination.+--+-- This operation may throw 'TLS.TLSException' on failure.+--+connectTLS :: ClientParams         -- ^ check "Data.TLSSetting"+           -> Maybe String         -- ^ Optional certificate subject name, if set to 'Nothing'+                                   -- then we will try to verify 'Network.Socket.HostName' as subject name+           -> N.HostName           -- ^ hostname to connect to+           -> N.PortNumber         -- ^ port number to connect to+           -> IO (Context, N.SockAddr)+connectTLS prms subname host port = do+    let subname' = maybe host id subname+        prms' = prms { TLS.clientServerIdentification = (subname', BC.pack (show port)) }+    (sock, addr) <- TCP.connectSocket host port+    E.bracketOnError (TLS.contextNew sock prms') closeTLS $ \ ctx -> do+        TLS.handshake ctx+        return (ctx, addr)++-- | Connect to server using TLS and return a t'Connection'.+--+connect :: ClientParams         -- ^ check "Data.TLSSetting"+        -> Maybe String         -- ^ Optional certificate subject name, if set to 'Nothing'+                                -- then we will try to verify 'Network.Socket.HostName' as subject name+        -> N.HostName           -- ^ hostname to connect to+        -> N.PortNumber         -- ^ port number to connect to+        -> IO TLSConnection+connect prms subname host port = connectTLS prms subname host port >>= tLsToConnection++-- | Accept a new TLS connection from remote client with listening socket.+--+-- This operation may throw 'TLS.TLSException' on failure.+--+accept :: ServerParams              -- ^ check "Data.TLSSetting"+       -> N.Socket                  -- ^ the listening 'Network.Socket.Socket'+       -> IO TLSConnection+accept prms sock = do+    (sock', addr) <- N.accept sock+    E.bracketOnError (TLS.contextNew sock' prms) closeTLS $ \ ctx -> do+        TLS.handshake ctx+        conn <- tLsToConnection (ctx, addr)+        return conn
+ test/Aeson.hs view
@@ -0,0 +1,338 @@+{-# LANGUAGE BangPatterns, CPP, OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-unused-binds #-}++module Aeson where++import Data.ByteString.Builder+  (Builder, byteString, toLazyByteString, charUtf8, word8)++import Control.Applicative (liftA2)+import Control.DeepSeq (NFData(..))+import Data.Attoparsec.ByteString.Char8 (Parser, char, endOfInput, scientific,+                                         skipSpace, string)+import Data.Bits ((.|.), shiftL)+import Data.ByteString (ByteString)+import Data.Char (chr)+import Data.Scientific (Scientific)+import Data.Text (Text)+import Data.Text.Encoding (decodeUtf8')+import Data.Vector as Vector (Vector, foldl', fromList)+import Data.Word (Word8)+import System.FilePath ((</>))+import qualified Data.Attoparsec.ByteString as A+import qualified Data.Attoparsec.Lazy as L+import qualified Data.Attoparsec.Zepto as Z+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as L+import qualified Data.ByteString.Unsafe as B+import qualified Data.HashMap.Strict as H+import System.Directory (doesDirectoryExist)++#define BACKSLASH 92+#define CLOSE_CURLY 125+#define CLOSE_SQUARE 93+#define COMMA 44+#define DOUBLE_QUOTE 34+#define OPEN_CURLY 123+#define OPEN_SQUARE 91+#define C_0 48+#define C_9 57+#define C_A 65+#define C_F 70+#define C_a 97+#define C_f 102+#define C_n 110+#define C_t 116++pathTo :: String -> IO FilePath+pathTo wat = do+  exists <- doesDirectoryExist "bench"+  return $ if exists+           then "bench" </> wat+           else wat++data Result a = Error String+              | Success a+                deriving (Eq, Show)+++-- | A JSON \"object\" (key\/value map).+type Object = H.HashMap Text Value++-- | A JSON \"array\" (sequence).+type Array = Vector Value++-- | A JSON value represented as a Haskell value.+data Value = Object !Object+           | Array !Array+           | String !Text+           | Number !Scientific+           | Bool !Bool+           | Null+             deriving (Eq, Show)++instance NFData Value where+    rnf (Object o) = rnf o+    rnf (Array a)  = Vector.foldl' (\x y -> rnf y `seq` x) () a+    rnf (String s) = rnf s+    rnf (Number n) = rnf n+    rnf (Bool b)   = rnf b+    rnf Null       = ()++-- | Parse a top-level JSON value.  This must be either an object or+-- an array, per RFC 4627.+--+-- The conversion of a parsed value to a Haskell value is deferred+-- until the Haskell value is needed.  This may improve performance if+-- only a subset of the results of conversions are needed, but at a+-- cost in thunk allocation.+json :: Parser Value+json = json_ object_ array_++-- | Parse a top-level JSON value.  This must be either an object or+-- an array, per RFC 4627.+--+-- This is a strict version of 'json' which avoids building up thunks+-- during parsing; it performs all conversions immediately.  Prefer+-- this version if most of the JSON data needs to be accessed.+json' :: Parser Value+json' = json_ object_' array_'++json_ :: Parser Value -> Parser Value -> Parser Value+json_ obj ary = do+  w <- skipSpace *> A.satisfy (\w -> w == OPEN_CURLY || w == OPEN_SQUARE)+  if w == OPEN_CURLY+    then obj+    else ary+{-# INLINE json_ #-}++object_ :: Parser Value+object_ = {-# SCC "object_" #-} Object <$> objectValues jstring value++object_' :: Parser Value+object_' = {-# SCC "object_'" #-} do+  !vals <- objectValues jstring' value'+  return (Object vals)+ where+  jstring' = do+    !s <- jstring+    return s++objectValues :: Parser Text -> Parser Value -> Parser (H.HashMap Text Value)+objectValues str val = do+  skipSpace+  let pair = liftA2 (,) (str <* skipSpace) (char ':' *> skipSpace *> val)+  H.fromList <$> commaSeparated pair CLOSE_CURLY+{-# INLINE objectValues #-}++array_ :: Parser Value+array_ = {-# SCC "array_" #-} Array <$> arrayValues value++array_' :: Parser Value+array_' = {-# SCC "array_'" #-} do+  !vals <- arrayValues value'+  return (Array vals)++commaSeparated :: Parser a -> Word8 -> Parser [a]+commaSeparated item endByte = do+  w <- A.peekWord8'+  if w == endByte+    then A.anyWord8 >> return []+    else loop+  where+    loop = do+      v <- item <* skipSpace+      ch <- A.satisfy $ \w -> w == COMMA || w == endByte+      if ch == COMMA+        then skipSpace >> (v:) <$> loop+        else return [v]+{-# INLINE commaSeparated #-}++arrayValues :: Parser Value -> Parser (Vector Value)+arrayValues val = do+  skipSpace+  Vector.fromList <$> commaSeparated val CLOSE_SQUARE+{-# INLINE arrayValues #-}++-- | Parse any JSON value.  You should usually 'json' in preference to+-- this function, as this function relaxes the object-or-array+-- requirement of RFC 4627.+--+-- In particular, be careful in using this function if you think your+-- code might interoperate with Javascript.  A na&#xef;ve Javascript+-- library that parses JSON data using @eval@ is vulnerable to attack+-- unless the encoded data represents an object or an array.  JSON+-- implementations in other languages conform to that same restriction+-- to preserve interoperability and security.+value :: Parser Value+value = do+  w <- A.peekWord8'+  case w of+    DOUBLE_QUOTE  -> A.anyWord8 *> (String <$> jstring_)+    OPEN_CURLY    -> A.anyWord8 *> object_+    OPEN_SQUARE   -> A.anyWord8 *> array_+    C_f           -> string "false" *> pure (Bool False)+    C_t           -> string "true" *> pure (Bool True)+    C_n           -> string "null" *> pure Null+    _              | w >= 48 && w <= 57 || w == 45+                  -> Number <$> scientific+      | otherwise -> fail "not a valid json value"++-- | Strict version of 'value'. See also 'json''.+value' :: Parser Value+value' = do+  w <- A.peekWord8'+  case w of+    DOUBLE_QUOTE  -> do+                     !s <- A.anyWord8 *> jstring_+                     return (String s)+    OPEN_CURLY    -> A.anyWord8 *> object_'+    OPEN_SQUARE   -> A.anyWord8 *> array_'+    C_f           -> string "false" *> pure (Bool False)+    C_t           -> string "true" *> pure (Bool True)+    C_n           -> string "null" *> pure Null+    _              | w >= 48 && w <= 57 || w == 45+                  -> do+                     !n <- scientific+                     return (Number n)+      | otherwise -> fail "not a valid json value"++-- | Parse a quoted JSON string.+jstring :: Parser Text+jstring = A.word8 DOUBLE_QUOTE *> jstring_++-- | Parse a string without a leading quote.+jstring_ :: Parser Text+jstring_ = {-# SCC "jstring_" #-} do+  s <- A.scan False $ \s c -> if s then Just False+                                   else if c == DOUBLE_QUOTE+                                        then Nothing+                                        else Just (c == BACKSLASH)+  _ <- A.word8 DOUBLE_QUOTE+  s1 <- if BACKSLASH `B.elem` s+        then case Z.parse unescape s of+            Right r  -> return r+            Left err -> fail err+         else return s++  case decodeUtf8' s1 of+      Right r  -> return r+      Left err -> fail $ show err++{-# INLINE jstring_ #-}++unescape :: Z.Parser ByteString+unescape = toByteString <$> go mempty where+  go acc = do+    h <- Z.takeWhile (/=BACKSLASH)+    let rest = do+          start <- Z.take 2+          let !slash = B.unsafeHead start+              !t = B.unsafeIndex start 1+              escape = case B.findIndex (==t) "\"\\/ntbrfu" of+                         Just i -> i+                         _      -> 255+          if slash /= BACKSLASH || escape == 255+            then fail "invalid JSON escape sequence"+            else do+            let cont m = go (acc `mappend` byteString h `mappend` m)+                {-# INLINE cont #-}+            if t /= 117 -- 'u'+              then cont (word8 (B.unsafeIndex mapping escape))+              else do+                   a <- hexQuad+                   if a < 0xd800 || a > 0xdfff+                     then cont (charUtf8 (chr a))+                     else do+                       b <- Z.string "\\u" *> hexQuad+                       if a <= 0xdbff && b >= 0xdc00 && b <= 0xdfff+                         then let !c = ((a - 0xd800) `shiftL` 10) ++                                       (b - 0xdc00) + 0x10000+                              in cont (charUtf8 (chr c))+                         else fail "invalid UTF-16 surrogates"+    done <- Z.atEnd+    if done+      then return (acc `mappend` byteString h)+      else rest+  mapping = "\"\\/\n\t\b\r\f"++hexQuad :: Z.Parser Int+hexQuad = do+  s <- Z.take 4+  let hex n | w >= C_0 && w <= C_9 = w - C_0+            | w >= C_a && w <= C_f = w - 87+            | w >= C_A && w <= C_F = w - 55+            | otherwise          = 255+        where w = fromIntegral $ B.unsafeIndex s n+      a = hex 0; b = hex 1; c = hex 2; d = hex 3+  if (a .|. b .|. c .|. d) /= 255+    then return $! d .|. (c `shiftL` 4) .|. (b `shiftL` 8) .|. (a `shiftL` 12)+    else fail "invalid hex escape"++decodeWith :: Parser Value -> (Value -> Result a) -> L.ByteString -> Maybe a+decodeWith p to s =+    case L.parse p s of+      L.Done _ v -> case to v of+                      Success a -> Just a+                      _         -> Nothing+      _          -> Nothing+{-# INLINE decodeWith #-}++decodeStrictWith :: Parser Value -> (Value -> Result a) -> B.ByteString+                 -> Maybe a+decodeStrictWith p to s =+    case either Error to (A.parseOnly p s) of+      Success a -> Just a+      Error _ -> Nothing+{-# INLINE decodeStrictWith #-}++eitherDecodeWith :: Parser Value -> (Value -> Result a) -> L.ByteString+                 -> Either String a+eitherDecodeWith p to s =+    case L.parse p s of+      L.Done _ v -> case to v of+                      Success a -> Right a+                      Error msg -> Left msg+      L.Fail _ _ msg -> Left msg+{-# INLINE eitherDecodeWith #-}++eitherDecodeStrictWith :: Parser Value -> (Value -> Result a) -> B.ByteString+                       -> Either String a+eitherDecodeStrictWith p to s =+    case either Error to (A.parseOnly p s) of+      Success a -> Right a+      Error msg -> Left msg+{-# INLINE eitherDecodeStrictWith #-}++-- $lazy+--+-- The 'json' and 'value' parsers decouple identification from+-- conversion.  Identification occurs immediately (so that an invalid+-- JSON document can be rejected as early as possible), but conversion+-- to a Haskell value is deferred until that value is needed.+--+-- This decoupling can be time-efficient if only a smallish subset of+-- elements in a JSON value need to be inspected, since the cost of+-- conversion is zero for uninspected elements.  The trade off is an+-- increase in memory usage, due to allocation of thunks for values+-- that have not yet been converted.++-- $strict+--+-- The 'json'' and 'value'' parsers combine identification with+-- conversion.  They consume more CPU cycles up front, but have a+-- smaller memory footprint.++-- | Parse a top-level JSON value followed by optional whitespace and+-- end-of-input.  See also: 'json'.+jsonEOF :: Parser Value+jsonEOF = json <* skipSpace <* endOfInput++-- | Parse a top-level JSON value followed by optional whitespace and+-- end-of-input.  See also: 'json''.+jsonEOF' :: Parser Value+jsonEOF' = json' <* skipSpace <* endOfInput++toByteString :: Builder -> ByteString+toByteString = L.toStrict . toLazyByteString+{-# INLINE toByteString #-}
+ test/AesonBP.hs view
@@ -0,0 +1,284 @@+{-# LANGUAGE BangPatterns, CPP, OverloadedStrings #-}+{-# OPTIONS_GHC -fno-warn-unused-binds #-}++module AesonBP where++import Data.ByteString.Builder+  (Builder, byteString, toLazyByteString, charUtf8, word8)++import Control.Applicative (liftA2)+import Data.Bits ((.|.), shiftL)+import Data.ByteString (ByteString)+import Data.Char (chr)+import Data.Text (Text)+import Data.Text.Encoding (decodeUtf8')+import Data.Vector as Vector (Vector, fromList)+import Data.Word (Word8)+import System.FilePath ((</>))+import qualified Data.Attoparsec.Zepto as Z+import Data.Binary.Get (Get)+import qualified Data.Binary.Parser as BP+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as L+import qualified Data.ByteString.Unsafe as B+import qualified Data.HashMap.Strict as H+import System.Directory (doesDirectoryExist)+import Aeson (Value(..))++#define BACKSLASH 92+#define CLOSE_CURLY 125+#define CLOSE_SQUARE 93+#define COMMA 44+#define COLON 58+#define DOUBLE_QUOTE 34+#define OPEN_CURLY 123+#define OPEN_SQUARE 91+#define C_0 48+#define C_9 57+#define C_A 65+#define C_F 70+#define C_a 97+#define C_f 102+#define C_n 110+#define C_t 116++pathTo :: String -> IO FilePath+pathTo wat = do+  exists <- doesDirectoryExist "bench"+  return $ if exists+           then "bench" </> wat+           else wat++data Result a = Error String+              | Success a+                deriving (Eq, Show)+++-- | A JSON \"object\" (key\/value map).+type Object = H.HashMap Text Value++-- | A JSON \"array\" (sequence).+type Array = Vector Value++-- | Parse a top-level JSON value.  This must be either an object or+-- an array, per RFC 4627.+--+-- The conversion of a parsed value to a Haskell value is deferred+-- until the Haskell value is needed.  This may improve performance if+-- only a subset of the results of conversions are needed, but at a+-- cost in thunk allocation.+json :: Get Value+json = json_ object_ array_++-- | Parse a top-level JSON value.  This must be either an object or+-- an array, per RFC 4627.+--+-- This is a strict version of 'json' which avoids building up thunks+-- during parsing; it performs all conversions immediately.  Prefer+-- this version if most of the JSON data needs to be accessed.+json' :: Get Value+json' = json_ object_' array_'++json_ :: Get Value -> Get Value -> Get Value+json_ obj ary = do+  w <- BP.skipSpaces *> BP.satisfy (\w -> w == OPEN_CURLY || w == OPEN_SQUARE)+  if w == OPEN_CURLY+    then obj+    else ary+{-# INLINE json_ #-}++object_ :: Get Value+object_ = {-# SCC "object_" #-} Object <$> objectValues jstring value++object_' :: Get Value+object_' = {-# SCC "object_'" #-} do+  !vals <- objectValues jstring' value'+  return (Object vals)+ where+  jstring' = do+    !s <- jstring+    return s++objectValues :: Get Text -> Get Value -> Get (H.HashMap Text Value)+objectValues str val = do+  BP.skipSpaces+  let pair = liftA2 (,) (str <* BP.skipSpaces) (BP.word8 COLON *> BP.skipSpaces *> val)+  H.fromList <$> commaSeparated pair CLOSE_CURLY+{-# INLINE objectValues #-}++array_ :: Get Value+array_ = {-# SCC "array_" #-} Array <$> arrayValues value++array_' :: Get Value+array_' = {-# SCC "array_'" #-} do+  !vals <- arrayValues value'+  return (Array vals)++commaSeparated :: Get a -> Word8 -> Get [a]+commaSeparated item endByte = do+  w <- BP.peek+  if w == endByte+    then BP.skipN 1 >> return []+    else loop+  where+    loop = do+      v <- item <* BP.skipSpaces+      ch <- BP.satisfy $ \w -> w == COMMA || w == endByte+      if ch == COMMA+        then BP.skipSpaces >> (v:) <$> loop+        else return [v]+{-# INLINE commaSeparated #-}++arrayValues :: Get Value -> Get (Vector Value)+arrayValues val = do+  BP.skipSpaces+  Vector.fromList <$> commaSeparated val CLOSE_SQUARE+{-# INLINE arrayValues #-}++-- | Parse any JSON value.  You should usually 'json' in preference to+-- this function, as this function relaxes the object-or-array+-- requirement of RFC 4627.+--+-- In particular, be careful in using this function if you think your+-- code might interoperate with Javascript.  A na&#xef;ve Javascript+-- library that parses JSON data using @eval@ is vulnerable to attack+-- unless the encoded data represents an object or an array.  JSON+-- implementations in other languages conform to that same restriction+-- to preserve interoperability and security.+value :: Get Value+value = do+  w <- BP.peek+  case w of+    DOUBLE_QUOTE  -> BP.skipN 1 *> (String <$> jstring_)+    OPEN_CURLY    -> BP.skipN 1 *> object_+    OPEN_SQUARE   -> BP.skipN 1 *> array_+    C_f           -> BP.string "false" *> pure (Bool False)+    C_t           -> BP.string "true" *> pure (Bool True)+    C_n           -> BP.string "null" *> pure Null+    _              | w >= 48 && w <= 57 || w == 45+                  -> Number <$> BP.scientific+      | otherwise -> fail "not a valid json value"++-- | Strict version of 'value'. See also 'json''.+value' :: Get Value+value' = do+  w <- BP.peek+  case w of+    DOUBLE_QUOTE  -> do+                     !s <- BP.skipN 1 *> jstring_+                     return (String s)+    OPEN_CURLY    -> BP.skipN 1 *> object_'+    OPEN_SQUARE   -> BP.skipN 1 *> array_'+    C_f           -> BP.string "false" *> pure (Bool False)+    C_t           -> BP.string "true" *> pure (Bool True)+    C_n           -> BP.string "null" *> pure Null+    _              | w >= 48 && w <= 57 || w == 45+                  -> do+                     !n <- BP.scientific+                     return (Number n)+      | otherwise -> fail "not a valid json value"++-- | Parse a quoted JSON string.+jstring :: Get Text+jstring = BP.word8 DOUBLE_QUOTE *> jstring_++-- | Parse a string without a leading quote.+jstring_ :: Get Text+jstring_ = {-# SCC "jstring_" #-} do+  s <- BP.scan False $ \s c -> if s then Just False+                                   else if c == DOUBLE_QUOTE+                                        then Nothing+                                        else Just (c == BACKSLASH)+  BP.word8 DOUBLE_QUOTE+  s1 <- if BACKSLASH `B.elem` s+        then case Z.parse unescape s of+            Right r  -> return r+            Left err -> fail err+         else return s++  case decodeUtf8' s1 of+      Right r  -> return r+      Left err -> fail $ show err++{-# INLINE jstring_ #-}++unescape :: Z.Parser ByteString+unescape = toByteString <$> go mempty where+  go acc = do+    h <- Z.takeWhile (/=BACKSLASH)+    let rest = do+          start <- Z.take 2+          let !slash = B.unsafeHead start+              !t = B.unsafeIndex start 1+              escape = case B.findIndex (==t) "\"\\/ntbrfu" of+                         Just i -> i+                         _      -> 255+          if slash /= BACKSLASH || escape == 255+            then fail "invalid JSON escape sequence"+            else do+            let cont m = go (acc `mappend` byteString h `mappend` m)+                {-# INLINE cont #-}+            if t /= 117 -- 'u'+              then cont (word8 (B.unsafeIndex mapping escape))+              else do+                   a <- hexQuad+                   if a < 0xd800 || a > 0xdfff+                     then cont (charUtf8 (chr a))+                     else do+                       b <- Z.string "\\u" *> hexQuad+                       if a <= 0xdbff && b >= 0xdc00 && b <= 0xdfff+                         then let !c = ((a - 0xd800) `shiftL` 10) ++                                       (b - 0xdc00) + 0x10000+                              in cont (charUtf8 (chr c))+                         else fail "invalid UTF-16 surrogates"+    done <- Z.atEnd+    if done+      then return (acc `mappend` byteString h)+      else rest+  mapping = "\"\\/\n\t\b\r\f"++hexQuad :: Z.Parser Int+hexQuad = do+  s <- Z.take 4+  let hex n | w >= C_0 && w <= C_9 = w - C_0+            | w >= C_a && w <= C_f = w - 87+            | w >= C_A && w <= C_F = w - 55+            | otherwise          = 255+        where w = fromIntegral $ B.unsafeIndex s n+      a = hex 0; b = hex 1; c = hex 2; d = hex 3+  if (a .|. b .|. c .|. d) /= 255+    then return $! d .|. (c `shiftL` 4) .|. (b `shiftL` 8) .|. (a `shiftL` 12)+    else fail "invalid hex escape"++-- $lazy+--+-- The 'json' and 'value' parsers decouple identification from+-- conversion.  Identification occurs immediately (so that an invalid+-- JSON document can be rejected as early as possible), but conversion+-- to a Haskell value is deferred until that value is needed.+--+-- This decoupling can be time-efficient if only a smallish subset of+-- elements in a JSON value need to be inspected, since the cost of+-- conversion is zero for uninspected elements.  The trade off is an+-- increase in memory usage, due to allocation of thunks for values+-- that have not yet been converted.++-- $strict+--+-- The 'json'' and 'value'' parsers combine identification with+-- conversion.  They consume more CPU cycles up front, but have a+-- smaller memory footprint.++-- | Parse a top-level JSON value followed by optional whitespace and+-- end-of-input.  See also: 'json'.+jsonEOF :: Get Value+jsonEOF = json <* BP.skipSpaces++-- | Parse a top-level JSON value followed by optional whitespace and+-- end-of-input.  See also: 'json''.+jsonEOF' :: Get Value+jsonEOF' = json' <* BP.skipSpaces++toByteString :: Builder -> ByteString+toByteString = L.toStrict . toLazyByteString+{-# INLINE toByteString #-}
+ test/BinLog.hs view
@@ -0,0 +1,234 @@+{-# LANGUAGE NegativeLiterals    #-}+{-# LANGUAGE ScopedTypeVariables #-}++module BinLog where++import           Control.Applicative+import           Control.Exception+import           Control.Monad+import           Data.Text.Encoding    (encodeUtf8)+import           Data.Time.Clock.POSIX+import           Data.Time.Format+import           Data.Time.LocalTime+import           Database.MySQL.Base+import           Database.MySQL.BinLog+import qualified System.IO.Streams     as Stream+import           Test.Tasty.HUnit++eventProducer :: IO ()+eventProducer = do+    c <- connect defaultConnectInfo {ciUser = "testMySQLHaskell", ciDatabase = "testMySQLHaskell"}+    execute_ c q1+    execute_ c q2+    return ()++tests :: MySQLConn -> Assertion+tests c = do+    Just blt <- getLastBinLogTracker c+    x@(fd, _, _) <- dumpBinLog c 1002 blt False+    rowEventStream <- decodeRowBinLogEvent x++    let Just t = parseTimeM True defaultTimeLocale  "%F %T" "2016-08-08 17:25:59" :: Maybe LocalTime+    z <- getCurrentTimeZone+    let timestamp = round $ utcTimeToPOSIXSeconds (localTimeToUTC z t)++    Just (RowUpdateEvent _ _ tme ue) <- Stream.read rowEventStream+    assertEqual "decode update event cloumn" (updateColumnCnt ue) 30+    assertEqual "decode update event rows" (updateRowData ue)+        [+            (+                [ BinLogLong 0+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                , BinLogNull+                ], [ BinLogLong 0+                , BinLogBit 224+                , BinLogTiny (-128)+                , BinLogTiny (-1)+                , BinLogShort (-32768)+                , BinLogShort (-1)+                , BinLogInt24 (-8388608)+                , BinLogInt24 (-1)+                , BinLogLong (-2147483648)+                , BinLogLong (-1)+                , BinLogLongLong (-9223372036854775808)+                , BinLogLongLong (-1)+                , BinLogNewDecimal 1.2345678900123456789e9+                , BinLogFloat 3.14159+                , BinLogDouble 3.1415926535+                , BinLogDate 2016 8 8+                , BinLogDateTime 2016 8 8 17 25 59+                , BinLogTimeStamp timestamp+                , BinLogTime 0 199 59 59+                , BinLogYear 1999+                , BinLogBytes "12345678"+                , BinLogBytes (encodeUtf8 "韩冬真赞")+                , BinLogBytes "12345678"+                , BinLogBytes "12345678"+                , BinLogBytes "12345678"+                , BinLogBytes (encodeUtf8 "韩冬真赞")+                , BinLogBytes "12345678"+                , BinLogBytes (encodeUtf8 "韩冬真赞")+                , BinLogEnum 1+                , BinLogSet 3+                ]+            )+        ]++    Just (RowUpdateEvent _ _ tme ue) <- Stream.read rowEventStream+    assertEqual "decode update event rows" (updateRowData ue)+        [+            (+                [ BinLogLong 0+                , BinLogBit 224   -- 0b11100000+                , BinLogTiny (-128)+                , BinLogTiny (-1)+                , BinLogShort (-32768)+                , BinLogShort (-1)+                , BinLogInt24 (-8388608)+                , BinLogInt24 (-1)+                , BinLogLong (-2147483648)+                , BinLogLong (-1)+                , BinLogLongLong (-9223372036854775808)+                , BinLogLongLong (-1)+                , BinLogNewDecimal 1.2345678900123456789e9+                , BinLogFloat 3.14159+                , BinLogDouble 3.1415926535+                , BinLogDate 2016 8 8+                , BinLogDateTime 2016 8 8 17 25 59+                , BinLogTimeStamp timestamp+                , BinLogTime 0 199 59 59+                , BinLogYear 1999+                , BinLogBytes "12345678"+                , BinLogBytes (encodeUtf8 "韩冬真赞")+                , BinLogBytes "12345678"+                , BinLogBytes "12345678"+                , BinLogBytes "12345678"+                , BinLogBytes (encodeUtf8 "韩冬真赞")+                , BinLogBytes "12345678"+                , BinLogBytes (encodeUtf8 "韩冬真赞")+                , BinLogEnum 1+                , BinLogSet 3+                ],  [ BinLogLong 0+                , BinLogBit 57514   -- 0b1110000010101010+                , BinLogTiny (-1)+                , BinLogTiny (-1)+                , BinLogShort (-32768)+                , BinLogShort (-1)+                , BinLogInt24 (-1)+                , BinLogInt24 (-1)+                , BinLogLong (-1)+                , BinLogLong (-1)+                , BinLogLongLong (-9223372036854775808)+                , BinLogLongLong (-1)+                , BinLogNewDecimal -1.2345678900123456789e9+                , BinLogFloat -3.14159+                , BinLogDouble -3.1415926535+                , BinLogDate 1800 8 8+                , BinLogDateTime 1800 8 8 17 25 59+                , BinLogTimeStamp timestamp+                , BinLogTime 1 199 59 59+                , BinLogYear 1901+                , BinLogBytes "12345678"+                , BinLogBytes (encodeUtf8 "韩冬真赞")+                , BinLogBytes "12345678"+                , BinLogBytes "12345678"+                , BinLogBytes "12345678"+                , BinLogBytes (encodeUtf8 "韩冬真赞")+                , BinLogBytes "12345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123"+                , BinLogBytes (encodeUtf8 "韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞")+                , BinLogEnum 1+                , BinLogSet 3+                ]+            )+        ]+q1 :: Query+q1 =  "UPDATE test SET \+            \__bit        = b'11100000'                            ,\+            \__tinyInt    = -128                                   ,\+            \__tinyIntU   = 255                                    ,\+            \__smallInt   = -32768                                 ,\+            \__smallIntU  = 65535                                  ,\+            \__mediumInt  = -8388608                               ,\+            \__mediumIntU = 16777215                               ,\+            \__int        = -2147483648                            ,\+            \__intU       = 4294967295                             ,\+            \__bigInt     = -9223372036854775808                   ,\+            \__bigIntU    = 18446744073709551615                   ,\+            \__decimal    = 1234567890.0123456789                  ,\+            \__float      = 3.14159                                ,\+            \__double     = 3.1415926535                           ,\+            \__date       = '2016-08-08'                           ,\+            \__datetime   = '2016-08-08 17:25:59'                  ,\+            \__timestamp  = '2016-08-08 17:25:59'                  ,\+            \__time       = '-199:59:59'                           ,\+            \__year       = 1999                                   ,\+            \__char       = '12345678'                             ,\+            \__varchar    = '韩冬真赞'                             ,\+            \__binary     = '12345678'                             ,\+            \__varbinary  = '12345678'                             ,\+            \__tinyblob   = '12345678'                             ,\+            \__tinytext   = '韩冬真赞'                             ,\+            \__blob       = '12345678'                             ,\+            \__text       = '韩冬真赞'                             ,\+            \__enum       = 'foo'                                  ,\+            \__set        = 'foo,bar' WHERE __id=0"+++q2 :: Query+q2 =  "UPDATE test SET \+            \__bit        = b'1110000010101010'                    ,\+            \__tinyInt    = -1                                     ,\+            \__tinyIntU   = 255                                    ,\+            \__smallInt   = -32768                                 ,\+            \__smallIntU  = 65535                                  ,\+            \__mediumInt  = -1                                     ,\+            \__mediumIntU = 16777215                               ,\+            \__int        = -1                                     ,\+            \__intU       = 4294967295                             ,\+            \__bigInt     = -9223372036854775808                   ,\+            \__bigIntU    = 18446744073709551615                   ,\+            \__decimal    = -1234567890.0123456789                 ,\+            \__float      = -3.14159                                ,\+            \__double     = -3.1415926535                           ,\+            \__date       = '1800-08-08'                           ,\+            \__datetime   = '1800-08-08 17:25:59'                  ,\+            \__timestamp  = '2016-08-08 17:25:59'                  ,\+            \__time       = '199:59:59'                            ,\+            \__year       = 1901                                   ,\+            \__char       = '12345678'                             ,\+            \__varchar    = '韩冬真赞'                             ,\+            \__binary     = '12345678'                             ,\+            \__varbinary  = '12345678'                             ,\+            \__tinyblob   = '12345678'                             ,\+            \__tinytext   = '韩冬真赞'                             ,\+            \__blob       = '12345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123456781234567812345678123'                             ,\+            \__text       = '韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞韩冬真赞'                              ,\+            \__enum       = 'foo'                                  ,\+            \__set        = 'foo,bar' WHERE __id=0"
+ test/BinLogNew.hs view
@@ -0,0 +1,101 @@+{-# LANGUAGE NegativeLiterals    #-}+{-# LANGUAGE ScopedTypeVariables #-}++module BinLogNew where++import           Control.Applicative+import           Control.Exception+import           Control.Monad+import           Data.Time.Clock.POSIX+import           Data.Time.Format+import           Data.Time.LocalTime+import           Database.MySQL.Base+import           Database.MySQL.BinLog+import qualified System.IO.Streams     as Stream+import           Test.Tasty.HUnit++eventProducer :: IO ()+eventProducer = do+    c <- connect defaultConnectInfo {ciUser = "testMySQLHaskell", ciDatabase = "testMySQLHaskell"}+    execute_ c q1+    execute_ c q2+    execute_ c q3+    return ()++tests :: MySQLConn -> Assertion+tests c = do+    Just blt <- getLastBinLogTracker c+    x@(fd, _, _) <- dumpBinLog c 1002 blt False+    rowEventStream <- decodeRowBinLogEvent x++    let Just t = parseTimeM True defaultTimeLocale  "%F %T%Q" "2016-08-08 17:25:59.1234" :: Maybe LocalTime+    z <- getCurrentTimeZone+    let timestamp = round $ utcTimeToPOSIXSeconds (localTimeToUTC z t)++    Just (RowUpdateEvent _ _ tme ue) <- Stream.read rowEventStream+    assertEqual "decode update event cloumn" (updateColumnCnt ue) 4+    assertEqual "decode update event rows" (updateRowData ue)+        [+            (+                [ BinLogLong 0+                , BinLogNull+                , BinLogNull+                , BinLogNull+                ], [ BinLogLong 0+                   , BinLogDateTime2 2016 8 8 17 25 59 120000+                   , BinLogTimeStamp2 timestamp 123400+                   , BinLogTime2 0 199 59 59 123456+                   ]+            )+        ]++    Just (RowUpdateEvent _ _ tme ue) <- Stream.read rowEventStream+    assertEqual "decode update event rows" (updateRowData ue)+        [+            (+                [ BinLogLong 0+                  , BinLogDateTime2 2016 8 8 17 25 59 120000+                  , BinLogTimeStamp2 timestamp 123400+                  , BinLogTime2 0 199 59 59 123456+                ], [ BinLogLong 0+                  , BinLogDateTime2 2016 8 8 17 25 59 100000+                  , BinLogTimeStamp2 timestamp 123000+                  , BinLogTime2 1 199 59 59 123450+                ]+            )+        ]++    Just (RowUpdateEvent _ _ tme ue) <- Stream.read rowEventStream+    assertEqual "decode update event rows" (updateRowData ue)+        [+            (+                [ BinLogLong 0+                  , BinLogDateTime2 2016 8 8 17 25 59 100000+                  , BinLogTimeStamp2 timestamp 123000+                  , BinLogTime2 1 199 59 59 123450+                ], [ BinLogLong 0+                  , BinLogNull+                  , BinLogNull+                  , BinLogTime2 0 0 59 59 123450+                ]+            )+        ]++q1 :: Query+q1 = "UPDATE test_new SET \+     \__datetime   = '2016-08-08 17:25:59.12'                  ,\+     \__timestamp  = '2016-08-08 17:25:59.1234'                ,\+     \__time       = '-199:59:59.123456' WHERE __id=0;"++q2 :: Query+q2 = "UPDATE test_new SET \+     \__datetime   = '2016-08-08 17:25:59.1'                  ,\+     \__timestamp  = '2016-08-08 17:25:59.123'                ,\+     \__time       = '199:59:59.12345' WHERE __id=0;"+++q3 :: Query+q3 = "UPDATE test_new SET \+     \__datetime   = null                  ,\+     \__timestamp  = null                  ,\+     \__time       = '-00:59:59.12345' WHERE __id=0;"
+ test/BinaryRow.hs view
@@ -0,0 +1,419 @@+{-# LANGUAGE NegativeLiterals    #-}+{-# LANGUAGE ScopedTypeVariables #-}++module BinaryRow where++import           Control.Applicative+import           Control.Exception+import           Control.Monad+import           Data.Time.Calendar  (fromGregorian)+import           Data.Time.LocalTime (LocalTime (..), TimeOfDay (..))+import           Database.MySQL.Base+import qualified System.IO.Streams   as Stream+import           Test.Tasty.HUnit+import qualified Data.Text as T+import qualified Data.ByteString as B+import qualified Data.Vector as V++tests :: MySQLConn -> Assertion+tests c = do+    selStmt <- prepareStmt c "SELECT * FROM test"++    (f, is) <- queryStmt c selStmt []+    assertEqual "decode Field types" (columnType <$> f)+        [ mySQLTypeLong+        , mySQLTypeBit+        , mySQLTypeTiny+        , mySQLTypeTiny+        , mySQLTypeShort+        , mySQLTypeShort+        , mySQLTypeInt24+        , mySQLTypeInt24+        , mySQLTypeLong+        , mySQLTypeLong+        , mySQLTypeLongLong+        , mySQLTypeLongLong+        , mySQLTypeNewDecimal+        , mySQLTypeFloat+        , mySQLTypeDouble+        , mySQLTypeDate+        , mySQLTypeDateTime+        , mySQLTypeTimestamp+        , mySQLTypeTime+        , mySQLTypeYear+        , mySQLTypeString+        , mySQLTypeVarString+        , mySQLTypeString+        , mySQLTypeVarString+        , mySQLTypeBlob+        , mySQLTypeBlob+        , mySQLTypeBlob+        , mySQLTypeBlob+        , mySQLTypeString+        , mySQLTypeString+        ]++    Just v <- Stream.read is+    assertEqual "decode NULL values" v+        [ MySQLInt32 0+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        ]++    Stream.skipToEof is+--------------------------------------------------------------------------------+    let bitV = 43744 -- 0b1010101011100000++    execute_ c "UPDATE test SET \+                \__bit        = b'1010101011100000'                    ,\+                \__tinyInt    = -128                                   ,\+                \__tinyIntU   = 255                                    ,\+                \__smallInt   = -32768                                 ,\+                \__smallIntU  = 65535                                  ,\+                \__mediumInt  = -8388608                               ,\+                \__mediumIntU = 16777215                               ,\+                \__int        = -2147483648                            ,\+                \__intU       = 4294967295                             ,\+                \__bigInt     = -9223372036854775808                   ,\+                \__bigIntU    = 18446744073709551615                   ,\+                \__decimal    = 1234567890.0123456789                  ,\+                \__float      = 3.14159                                ,\+                \__double     = 3.1415926535                           ,\+                \__date       = '2016-08-08'                           ,\+                \__datetime   = '2016-08-08 17:25:59'                  ,\+                \__timestamp  = '2016-08-08 17:25:59'                  ,\+                \__time       = '-199:59:59'                           ,\+                \__year       = 1999                                   ,\+                \__char       = '12345678'                             ,\+                \__varchar    = '韩冬真赞'                             ,\+                \__binary     = '12345678'                             ,\+                \__varbinary  = '12345678'                             ,\+                \__tinyblob   = '12345678'                             ,\+                \__tinytext   = '韩冬真赞'                             ,\+                \__blob       = '12345678'                             ,\+                \__text       = '韩冬真赞'                             ,\+                \__enum       = 'foo'                                  ,\+                \__set        = 'foo,bar' WHERE __id=0"++    (_, is) <- queryStmt c selStmt []+    Just v <- Stream.read is++    assertEqual "decode binary protocol" v+        [ MySQLInt32 0+        , MySQLBit bitV+        , MySQLInt8 (-128)+        , MySQLInt8U 255+        , MySQLInt16 (-32768)+        , MySQLInt16U 65535+        , MySQLInt32 (-8388608)+        , MySQLInt32U 16777215+        , MySQLInt32 (-2147483648)+        , MySQLInt32U 4294967295+        , MySQLInt64 (-9223372036854775808)+        , MySQLInt64U 18446744073709551615+        , MySQLDecimal 1234567890.0123456789+        , MySQLFloat 3.14159+        , MySQLDouble 3.1415926535+        , MySQLDate (fromGregorian 2016 08 08)+        , MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59))+        , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59))+        , MySQLTime 1 (TimeOfDay 199 59 59)+        , MySQLYear 1999+        , MySQLText "12345678"+        , MySQLText "韩冬真赞"+        , MySQLBytes "12345678"+        , MySQLBytes "12345678"+        , MySQLBytes "12345678"+        , MySQLText "韩冬真赞"+        , MySQLBytes "12345678"+        , MySQLText "韩冬真赞"+        , MySQLText "foo"+        , MySQLText "foo,bar"]+    Stream.skipToEof is++    (_, is') <- queryStmtVector c selStmt []+    Just v' <- Stream.read is'+    Stream.skipToEof is'++    assertEqual "decode binary protocol(queryStmtVector)" v (V.toList v')++--------------------------------------------------------------------------------+    updStmt <- prepareStmt c+            "UPDATE test SET \+            \__bit        = ?     ,\+            \__tinyInt    = ?     ,\+            \__tinyIntU   = ?     ,\+            \__smallInt   = ?     ,\+            \__smallIntU  = ?     ,\+            \__mediumInt  = ?     ,\+            \__mediumIntU = ?     ,\+            \__int        = ?     ,\+            \__intU       = ?     ,\+            \__bigInt     = ?     ,\+            \__bigIntU    = ?     ,\+            \__decimal    = ?     ,\+            \__float      = ?     ,\+            \__double     = ?     ,\+            \__date       = ?     ,\+            \__datetime   = ?     ,\+            \__timestamp  = ?     ,\+            \__time       = ?     ,\+            \__year       = ?     ,\+            \__char       = ?     ,\+            \__varchar    = ?     ,\+            \__binary     = ?     ,\+            \__varbinary  = ?     ,\+            \__tinyblob   = ?     ,\+            \__tinytext   = ?     ,\+            \__blob       = ?     ,\+            \__text       = ?     ,\+            \__enum       = ?     ,\+            \__set        = ? WHERE __id=0"++    executeStmt c updStmt+                [ MySQLBit bitV+                , MySQLInt8 (-128)+                , MySQLInt8U 255+                , MySQLInt16 (-32768)+                , MySQLInt16U 65535+                , MySQLInt32 (-8388608)+                , MySQLInt32U 16777215+                , MySQLInt32 (-2147483648)+                , MySQLInt32U 4294967295+                , MySQLInt64 (-9223372036854775808)+                , MySQLInt64U 18446744073709551615+                , MySQLDecimal 1234567890.0123456789+                , MySQLFloat 3.14159+                , MySQLDouble 3.1415926535+                , MySQLDate (fromGregorian 2016 08 08)+                , MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59))+                , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59))+                , MySQLTime 1 (TimeOfDay 199 59 59)+                , MySQLYear 1999+                , MySQLText "12345678"+                , MySQLText "韩冬真赞"+                , MySQLBytes "12345678"+                , MySQLBytes "12345678"+                , MySQLBytes "12345678"+                , MySQLText "韩冬真赞"+                , MySQLBytes "12345678"+                , MySQLText "韩冬真赞"+                , MySQLText "foo"+                , MySQLText "foo,bar"+                ]++++    (_, is) <- queryStmt c selStmt []+    Just v <- Stream.read is++    assertEqual "roundtrip binary protocol" v+        [ MySQLInt32 0+        , MySQLBit bitV+        , MySQLInt8 (-128)+        , MySQLInt8U 255+        , MySQLInt16 (-32768)+        , MySQLInt16U 65535+        , MySQLInt32 (-8388608)+        , MySQLInt32U 16777215+        , MySQLInt32 (-2147483648)+        , MySQLInt32U 4294967295+        , MySQLInt64 (-9223372036854775808)+        , MySQLInt64U 18446744073709551615+        , MySQLDecimal 1234567890.0123456789+        , MySQLFloat 3.14159+        , MySQLDouble 3.1415926535+        , MySQLDate (fromGregorian 2016 08 08)+        , MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59))+        , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59))+        , MySQLTime 1 (TimeOfDay 199 59 59)+        , MySQLYear 1999+        , MySQLText "12345678"+        , MySQLText "韩冬真赞"+        , MySQLBytes "12345678"+        , MySQLBytes "12345678"+        , MySQLBytes "12345678"+        , MySQLText "韩冬真赞"+        , MySQLBytes "12345678"+        , MySQLText "韩冬真赞"+        , MySQLText "foo"+        , MySQLText "foo,bar"+        ]++    Stream.skipToEof is+--------------------------------------------------------------------------------+    execute_ c "UPDATE test SET \+        \__mediumInt  = null         ,\+        \__double     = null         ,\+        \__text = null WHERE __id=0"++    (_, is) <- queryStmt c selStmt []+    Just v <- Stream.read is++    assertEqual "decode binary protocol with null" v+        [ MySQLInt32 0+        , MySQLBit bitV+        , MySQLInt8 (-128)+        , MySQLInt8U 255+        , MySQLInt16 (-32768)+        , MySQLInt16U 65535+        , MySQLNull+        , MySQLInt32U 16777215+        , MySQLInt32 (-2147483648)+        , MySQLInt32U 4294967295+        , MySQLInt64 (-9223372036854775808)+        , MySQLInt64U 18446744073709551615+        , MySQLDecimal 1234567890.0123456789+        , MySQLFloat 3.14159+        , MySQLNull+        , MySQLDate (fromGregorian 2016 08 08)+        , MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59))+        , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59))+        , MySQLTime 1 (TimeOfDay 199 59 59)+        , MySQLYear 1999+        , MySQLText "12345678"+        , MySQLText "韩冬真赞"+        , MySQLBytes "12345678"+        , MySQLBytes "12345678"+        , MySQLBytes "12345678"+        , MySQLText "韩冬真赞"+        , MySQLBytes "12345678"+        , MySQLNull+        , MySQLText "foo"+        , MySQLText "foo,bar"+        ]++    Stream.skipToEof is+--------------------------------------------------------------------------------+    updStmt1 <- prepareStmt c "UPDATE test SET \+        \__decimal  = ?         ,\+        \__date     = ?         ,\+        \__timestamp = ? WHERE __id=0"+    executeStmt c updStmt1 [MySQLNull, MySQLNull, MySQLNull]++    (_, is) <- queryStmt c selStmt []+    Just v <- Stream.read is++    assertEqual "roundtrip binary protocol with null" v+        [ MySQLInt32 0+        , MySQLBit bitV+        , MySQLInt8 (-128)+        , MySQLInt8U 255+        , MySQLInt16 (-32768)+        , MySQLInt16U 65535+        , MySQLNull+        , MySQLInt32U 16777215+        , MySQLInt32 (-2147483648)+        , MySQLInt32U 4294967295+        , MySQLInt64 (-9223372036854775808)+        , MySQLInt64U 18446744073709551615+        , MySQLNull+        , MySQLFloat 3.14159+        , MySQLNull+        , MySQLNull+        , MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59))+        , MySQLNull+        , MySQLTime 1 (TimeOfDay 199 59 59)+        , MySQLYear 1999+        , MySQLText "12345678"+        , MySQLText "韩冬真赞"+        , MySQLBytes "12345678"+        , MySQLBytes "12345678"+        , MySQLBytes "12345678"+        , MySQLText "韩冬真赞"+        , MySQLBytes "12345678"+        , MySQLNull+        , MySQLText "foo"+        , MySQLText "foo,bar"+        ]+--------------------------------------------------------------------------------+    Stream.skipToEof is+    execute_ c "UPDATE test SET \+        \__time       = '199:59:59'     ,\+        \__year       = 0  WHERE __id=0"++    selStmt2 <- prepareStmt c "SELECT __time, __year FROM test"+    (_, is) <- queryStmt c selStmt2 []+    Just v <- Stream.read is++    assertEqual "decode binary protocol 2" v+            [ MySQLTime 0 (TimeOfDay 199 59 59)+            , MySQLYear 0+            ]++    Stream.skipToEof is+--------------------------------------------------------------------------------+    updStmt2 <- prepareStmt c "UPDATE test SET \+                \__time       = ?     ,\+                \__year       = ?  WHERE __id=0"++    executeStmt c updStmt2 [ MySQLTime 0 (TimeOfDay 00 00 00), MySQLYear 2055]++    (_, is) <- queryStmt c selStmt2 []+    Just v <- Stream.read is+    assertEqual "roundtrip binary protocol 2" v+            [ MySQLTime 0 (TimeOfDay 00 00 00)+            , MySQLYear 2055+            ]++    Stream.skipToEof is+--------------------------------------------------------------------------------+    execute_ c "UPDATE test SET \+        \__text       = ''     ,\+        \__blob       = ''  WHERE __id=0"++    selStmt3 <- prepareStmt c "SELECT __text, __blob FROM test"+    (_, is) <- queryStmt c selStmt3 []+    Just v <- Stream.read is++    assertEqual "decode binary protocol 3" v+            [ MySQLText ""+            , MySQLBytes ""+            ]++    Stream.skipToEof is+--------------------------------------------------------------------------------+    updStmt3 <- prepareStmt c "UPDATE test SET \+                \__text       = ?     ,\+                \__blob       = ?  WHERE __id=0"++    executeStmt c updStmt3+        [ MySQLText (T.replicate 100000 "xyz")+        , MySQLBytes (B.replicate 1000000 64)+        ]++    (_, is) <- queryStmt c selStmt3 []+    Just v <- Stream.read is+    assertEqual "roundtrip binary protocol 3" v+        [ MySQLText (T.replicate 100000 "xyz")+        , MySQLBytes (B.replicate 1000000 64)+        ]++    Stream.skipToEof is
+ test/BinaryRowNew.hs view
@@ -0,0 +1,111 @@+{-# LANGUAGE NegativeLiterals    #-}+{-# LANGUAGE ScopedTypeVariables #-}++module BinaryRowNew where++import           Control.Applicative+import           Data.Time.Calendar  (fromGregorian)+import           Data.Time.LocalTime (LocalTime (..), TimeOfDay (..))+import           Database.MySQL.Base+import qualified System.IO.Streams   as Stream+import           Test.Tasty.HUnit++tests :: MySQLConn -> Assertion+tests c = do+    selStmt <- prepareStmt c "SELECT * FROM test_new"++    (f, is) <- queryStmt c selStmt []+    assertEqual "decode Field types" (columnType <$> f)+        [ mySQLTypeLong+        , mySQLTypeDateTime+        , mySQLTypeTimestamp+        , mySQLTypeTime+        ]++    Just v <- Stream.read is+    assertEqual "decode NULL values" v+        [ MySQLInt32 0+        , MySQLNull+        , MySQLNull+        , MySQLNull+        ]++    Stream.skipToEof is++    execute_ c "UPDATE test_new SET \+                \__datetime   = '2016-08-08 17:25:59.12'                  ,\+                \__timestamp  = '2016-08-08 17:25:59.1234'                ,\+                \__time       = '-199:59:59.123456' WHERE __id=0"++    (_, is) <- queryStmt c selStmt []+    Just v <- Stream.read is++    assertEqual "decode binary protocol" v+        [ MySQLInt32 0+        , MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.12))+        , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.1234))+        , MySQLTime 1 (TimeOfDay 199 59 59.123456)+        ]++    Stream.skipToEof is++    execute_ c "UPDATE test_new SET \+                \__datetime   = '2016-08-08 17:25:59.1'                  ,\+                \__timestamp  = '2016-08-08 17:25:59.12'                ,\+                \__time       = '199:59:59.123' WHERE __id=0"++    (_, is) <- queryStmt c selStmt []+    Just v <- Stream.read is++    assertEqual "decode binary protocol 2" v+        [ MySQLInt32 0+        , MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.1))+        , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.12))+        , MySQLTime 0 (TimeOfDay 199 59 59.123)+        ]++    Stream.skipToEof is++    updStmt <- prepareStmt c+            "UPDATE test_new SET \+            \__datetime   = ?     ,\+            \__timestamp  = ?     ,\+            \__time       = ? WHERE __id=0"++    executeStmt c updStmt+                [ MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.12))+                , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.1234))+                , MySQLTime 1 (TimeOfDay 199 59 59.123456)+                ]+++    (_, is) <- queryStmt c selStmt []+    Just v <- Stream.read is++    assertEqual "roundtrip binary protocol" v+        [ MySQLInt32 0+        , MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.12))+        , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.1234))+        , MySQLTime 1 (TimeOfDay 199 59 59.123456)+        ]++    Stream.skipToEof is++    executeStmt c updStmt+                [ MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.1))+                , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.12))+                , MySQLTime 0 (TimeOfDay 199 59 59.1234)+                ]+++    (_, is) <- queryStmt c selStmt []+    Just v <- Stream.read is++    assertEqual "roundtrip binary protocol 2" v+        [ MySQLInt32 0+        , MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.1))+        , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.12))+        , MySQLTime 0 (TimeOfDay 199 59 59.1234)+        ]++    Stream.skipToEof is
+ test/BoundsCheck.hs view
@@ -0,0 +1,140 @@+module BoundsCheck (tests) where++import qualified Data.ByteString             as B+import qualified Data.ByteString.Lazy        as L+import           Data.Binary.Put             (runPut, putWord8)+import           Data.Binary.Parser          (parseOnly)+import           Test.Tasty+import           Test.Tasty.HUnit++import           Database.MySQL.Protocol.ColumnDef+import           Database.MySQL.Protocol.MySQLValue (getTextField, MySQLValue)+import           Database.MySQL.Protocol.Packet (putLenEncInt)+import           Database.MySQL.BinLogProtocol.BinLogEvent+                    (FormatDescription(..), eventHeaderLen, BinLogEventType(..))+import           Database.MySQL.BinLogProtocol.BinLogValue (getBinLogField)+import           Database.MySQL.BinLogProtocol.BinLogMeta (BinLogMeta(..))++tests :: TestTree+tests = testGroup "bounds-check"+    [ testGroup "text-protocol" textProtocolTests+    , testGroup "binlog-value" binlogValueTests+    , testGroup "binlog-event" binlogEventTests+    ]++--------------------------------------------------------------------------------+-- Helpers++-- | Build a ColumnDef with the given field type and default flags.+mkColumnDef :: FieldType -> ColumnDef+mkColumnDef ft = ColumnDef+    { columnDB        = ""+    , columnTable     = ""+    , columnOrigTable = ""+    , columnName      = ""+    , columnOrigName  = ""+    , columnCharSet   = 63 -- binary+    , columnLength    = 0+    , columnType      = ft+    , columnFlags     = 0+    , columnDecimals  = 0+    }++-- | Encode a length-prefixed ByteString as the MySQL wire protocol does,+-- then run the given Get parser on it.+parseLenEncField :: ColumnDef -> B.ByteString -> Either String MySQLValue+parseLenEncField cd payload =+    let encoded = L.toStrict . runPut $ do+            putLenEncInt (B.length payload)+            mapM_ putWord8 (B.unpack payload)+    in parseOnly (getTextField cd) encoded++-- | Check that parsing yields a Left (failure), not a crash or garbage.+assertParseFailure :: String -> Either String a -> Assertion+assertParseFailure label result = case result of+    Left _  -> pure ()+    Right _ -> assertFailure (label ++ ": expected parse failure but got a result")++--------------------------------------------------------------------------------+-- Text protocol tests++textProtocolTests :: [TestTree]+textProtocolTests =+    [ testCase "timestamp: date-only no time part" $ do+        -- "2024-01-01" is 10 bytes. dateParser succeeds, then+        -- unsafeDrop 11 on a 10-byte string is UB.+        -- With "abc" this wouldn't trigger because readDecimal "abc" → Nothing+        -- short-circuits via Maybe's Applicative before unsafeDrop is forced.+        let cd = mkColumnDef mySQLTypeTimestamp+            result = parseLenEncField cd "2024-01-01"+        assertParseFailure "timestamp no time" result++    , testCase "timestamp: truncated time" $ do+        -- "2024-01-01 1" is 12 bytes. dateParser succeeds, unsafeDrop 11+        -- gives "1", timeParser reads hour=1, rest="", then unsafeTail+        -- on empty remainder is UB.+        let cd = mkColumnDef mySQLTypeTimestamp+            result = parseLenEncField cd "2024-01-01 1"+        assertParseFailure "timestamp truncated time" result++    , testCase "datetime: date-only no time part" $ do+        -- Same as timestamp: dateParser succeeds on "2024-01-01",+        -- then unsafeDrop 11 on 10-byte string is UB.+        let cd = mkColumnDef mySQLTypeDateTime+            result = parseLenEncField cd "2024-01-01"+        assertParseFailure "datetime no time" result++    , testCase "time: empty input" $ do+        -- TIME parser does unsafeIndex 0 on the payload; empty = UB+        let cd = mkColumnDef mySQLTypeTime+            result = parseLenEncField cd ""+        assertParseFailure "time empty" result++    , testCase "date: missing separator" $ do+        -- "2024" has no '-' separator; readDecimal consumes everything,+        -- then unsafeTail is called on an empty remainder+        let cd = mkColumnDef mySQLTypeDate+            result = parseLenEncField cd "2024"+        assertParseFailure "date missing sep" result++    , testCase "time: missing separator" $ do+        -- "12" has no ':' separator; readDecimal consumes it,+        -- then unsafeTail is called on empty remainder+        let cd = mkColumnDef mySQLTypeTime+            result = parseLenEncField cd "12"+        assertParseFailure "time missing sep" result+    ]++--------------------------------------------------------------------------------+-- BinLog value tests++binlogValueTests :: [TestTree]+binlogValueTests =+    [ testCase "NEWDECIMAL: precision < scale causes underflow" $ do+        -- precision=3, scale=10: (precision - scale) underflows Word8 to 249,+        -- producing a huge index into the 10-element sizeTable+        let meta = BINLOG_TYPE_NEWDECIMAL 3 10+            -- Provide some bytes so Get doesn't fail on "not enough input" first+            input = B.replicate 64 0x80+            result = parseOnly (getBinLogField meta) input+        assertParseFailure "NEWDECIMAL underflow" result+    ]++--------------------------------------------------------------------------------+-- BinLog event tests++binlogEventTests :: [TestTree]+binlogEventTests =+    [ testCase "eventHeaderLen: BINLOG_UNKNOWN_EVENT (negative index)" $ do+        -- fromEnum BINLOG_UNKNOWN_EVENT - 1 = -1, negative index into ByteString+        let fd = FormatDescription+                { fdVersion = 4+                , fdMySQLVersion = B.replicate 50 0+                , fdCreateTime = 0+                , fdEventHeaderLenVector = B.pack [19, 19, 19]  -- short vector+                }+            -- This should not crash; should return safe fallback+            result = eventHeaderLen fd BINLOG_UNKNOWN_EVENT+        -- After fix: returns 0 (safe fallback) instead of crashing+        assertEqual "unknown event should return 0" 0 result+    ]
+ test/CachingSha2.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE ScopedTypeVariables #-}++module CachingSha2 (tests) where++import Database.MySQL.Base+import qualified System.IO.Streams as Stream+import Test.Tasty+import Test.Tasty.HUnit++-- | These tests exercise two different authentication paths in 'completeAuth'.+-- Both tests connect and run @SELECT 1@, but the server-side auth protocol+-- differs based on which MySQL user is used. The users are created in+-- @nix/ci.nix@ (integrated-checks-mysql80) with different auth plugins:+--+-- * @testMySQLHaskellSha2@ — created with @caching_sha2_password@ (the MySQL 8.0+--   default). The client sends a SHA256 scramble, the server responds with+--   AuthMoreData (0x01, byte 2 = 0x03) indicating fast auth success.+--   The CI script pre-caches the verifier via a unix socket login so the+--   fast path is guaranteed.+--+-- * @testMySQLHaskellNative@ — created with @mysql_native_password@. The server+--   advertises @caching_sha2_password@ in its Greeting, so the client initially+--   sends a SHA256 scramble. The server then responds with AuthSwitchRequest+--   (0xFE) telling the client to re-authenticate using @mysql_native_password@+--   with a new salt. The client re-scrambles with SHA1 and sends the response.+tests :: TestTree+tests = testGroup "caching_sha2_password"+    [ testCaseSteps "SHA256 fast auth" $ \step -> do+        step "connecting as testMySQLHaskellSha2 (caching_sha2_password)..."+        (_, c) <- connectDetail defaultConnectInfo+            { ciUser = "testMySQLHaskellSha2"+            , ciPassword = "testPassword123"+            , ciDatabase = "testMySQLHaskell"+            }++        step "executing SELECT 1..."+        (_, is) <- query_ c "SELECT 1"+        Just row <- Stream.read is+        assertBool "SELECT 1 returns 1" (row == [MySQLInt32 1] || row == [MySQLInt64 1])+        Stream.skipToEof is++        close c++    , testCaseSteps "AuthSwitchRequest handling (mysql_native_password on sha2 server)" $ \step -> do+        step "connecting as testMySQLHaskellNative (mysql_native_password)..."+        (_, c) <- connectDetail defaultConnectInfo+            { ciUser = "testMySQLHaskellNative"+            , ciPassword = "nativePass123"+            , ciDatabase = "testMySQLHaskell"+            }++        step "executing SELECT 1..."+        (_, is) <- query_ c "SELECT 1"+        Just row <- Stream.read is+        assertBool "SELECT 1 returns 1" (row == [MySQLInt32 1] || row == [MySQLInt64 1])+        Stream.skipToEof is++        close c+    ]
+ test/ConnectInfo.hs view
@@ -0,0 +1,34 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Configuration diagnostics must never inspect or reveal password bytes.+module ConnectInfo (tests) where++import Control.Exception (evaluate)+import Control.Monad (forM_)+import qualified Data.ByteString as B+import Data.List (isInfixOf)+import Database.MySQL.Base+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.HUnit (assertBool, testCase, (@?=))++tests :: TestTree+tests = testGroup "connection diagnostics"+    [ testCase "redact empty, textual and arbitrary binary passwords" $+        forM_ [B.empty, "private-password-42", B.pack [0..255]] $ \password -> do+            let ci = defaultConnectInfo {ciPassword = password}+                rendered = show ci+            assertBool "password placeholder" ("ciPassword = <redacted>" `isInfixOf` rendered)+            if B.null password then pure () else+                assertBool "password representation is absent" (not (show password `isInfixOf` rendered))+            ciPassword ci @?= password+    , testCase "redaction does not evaluate a password" $ do+        let ci = defaultConnectInfo {ciPassword = error "password was inspected"}+        _ <- evaluate (length (show ci))+        pure ()+    , testCase "retain useful endpoint and account diagnostics" $ do+        let ci = defaultConnectInfo+                {ciHost = "db.example.com", ciPort = 3307, ciUser = "app", ciDatabase = "sample"}+        show ci @?= "ConnectInfo {ciHost = \"db.example.com\", ciPort = 3307, ciDatabase = \"sample\", ciUser = \"app\", ciPassword = <redacted>, ciCharset = 224}"+    , testCase "respect Show precedence without exposing credentials" $+        showsPrec 11 defaultConnectInfo "tail" @?= "(" ++ show defaultConnectInfo ++ ")tail"+    ]
+ test/ExecuteMany.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE NegativeLiterals    #-}+{-# LANGUAGE ScopedTypeVariables #-}++module ExecuteMany where++import           Data.Time.Calendar  (fromGregorian)+import           Data.Time.LocalTime (LocalTime (..), TimeOfDay (..))+import           Database.MySQL.Base+import           Test.Tasty.HUnit++tests :: MySQLConn -> Assertion+tests c = do++    oks <- withTransaction c $ executeMany c "INSERT INTO test VALUES(\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?     ,\+            \?)"+            (replicate 50000+                [ MySQLInt32 0+                , MySQLBit 255+                , MySQLInt8 (-128)+                , MySQLInt8U 255+                , MySQLInt16 (-32768)+                , MySQLInt16U 65535+                , MySQLInt32 (-8388608)+                , MySQLInt32U 16777215+                , MySQLInt32 (-2147483648)+                , MySQLInt32U 4294967295+                , MySQLInt64 (-9223372036854775808)+                , MySQLInt64U 18446744073709551615+                , MySQLDecimal 1234567890.0123456789+                , MySQLFloat 3.14159+                , MySQLDouble 3.1415926535+                , MySQLDate (fromGregorian 2016 08 08)+                , MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59))+                , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59))+                , MySQLTime 1 (TimeOfDay 199 59 59)+                , MySQLYear 1999+                , MySQLText "12345678"+                , MySQLText "韩冬真赞"+                , MySQLBytes "12345678"+                , MySQLBytes "12345678"+                , MySQLBytes "12345678"+                , MySQLText "韩冬真赞"+                , MySQLBytes "12345678"+                , MySQLText "韩冬真赞"+                , MySQLText "foo"+                , MySQLText "foo,bar"+                ]+            )+    assertEqual "executeMany affected rows" (sum $ map okAffectedRows oks) 50000
+ test/FieldRoundtrip.hs view
@@ -0,0 +1,91 @@+{-# LANGUAGE TypeApplications #-}++-- | Roundtrip law for every 'Field' instance+-- (@fromField (toField x) == Right x@), plus the decode-failure behaviour+-- that a roundtrip cannot exercise.+module FieldRoundtrip (tests) where++import           Data.ByteString                    (ByteString)+import qualified Data.ByteString.Lazy               as LazyByteString+import           Data.Int                           (Int16, Int32, Int64, Int8)+import           Data.Scientific                    (Scientific)+import           Data.Text                          (Text)+import qualified Data.Text.Lazy                     as LazyText+import           Data.Time.Calendar                 (Day)+import           Data.Time.LocalTime                (LocalTime,+                                                     TimeOfDay (..))+import           Data.Word                          (Word16, Word32, Word64,+                                                     Word8)+import           Database.MySQL.Field+import           Database.MySQL.Protocol.MySQLValue (MySQLValue (..))+import           Test.QuickCheck.Instances          ()+import           Test.Tasty+import           Test.Tasty.HUnit                   (testCase, (@?=))+import           Test.Tasty.QuickCheck              (Property, testProperty,+                                                     (===))++roundtrips :: (Field a, Eq a, Show a) => a -> Property+roundtrips value = fromField (toField value) === Right value++tests :: TestTree+tests = testGroup "Field"+    [ testGroup "roundtrip"+        [ testProperty "Int8" (roundtrips @Int8)+        , testProperty "Word8" (roundtrips @Word8)+        , testProperty "Int16" (roundtrips @Int16)+        , testProperty "Word16" (roundtrips @Word16)+        , testProperty "Int32" (roundtrips @Int32)+        , testProperty "Word32" (roundtrips @Word32)+        , testProperty "Int64" (roundtrips @Int64)+        , testProperty "Word64" (roundtrips @Word64)+        , testProperty "Float" (roundtrips @Float)+        , testProperty "Double" (roundtrips @Double)+        , testProperty "Scientific" (roundtrips @Scientific)+        , testProperty "Text" (roundtrips @Text)+        , testProperty "lazy Text" (roundtrips @LazyText.Text)+        , testProperty "String" (roundtrips @String)+        , testProperty "ByteString" (roundtrips @ByteString)+        , testProperty "lazy ByteString" (roundtrips @LazyByteString.ByteString)+        , testProperty "Day" (roundtrips @Day)+        , testProperty "LocalTime" (roundtrips @LocalTime)+        , testProperty "TimeOfDay" (roundtrips @TimeOfDay)+        , testProperty "Bool" (roundtrips @Bool)+        , testProperty "Maybe Int32" (roundtrips @(Maybe Int32))+        , testProperty "Maybe Text" (roundtrips @(Maybe Text))+        ]+    , testGroup "decode failures"+        [ testCase "NULL into a non-Maybe type is DecodeUnexpectedNull" $+            decodeText MySQLNull+                @?= Left (DecodeUnexpectedNull (ExpectedTypeName "Text"))+        , testCase "wrong constructor is DecodeTypeMismatch" $+            decodeInt8 (MySQLText "hi")+                @?= Left (DecodeTypeMismatch (ExpectedTypeName "Int8")+                                             (MySQLText "hi"))+        , testCase "no implicit widening: INT32 value into Int64 fails" $+            decodeInt64 (MySQLInt32 42)+                @?= Left (DecodeTypeMismatch (ExpectedTypeName "Int64")+                                             (MySQLInt32 42))+        , testCase "negative TIME into TimeOfDay fails" $+            decodeTimeOfDay (MySQLTime 1 (TimeOfDay 1 2 3))+                @?= Left (DecodeTypeMismatch (ExpectedTypeName "TimeOfDay")+                                             (MySQLTime 1 (TimeOfDay 1 2 3)))+        ]+    , testGroup "decode alternatives"+        [ testCase "NULL into Maybe is Nothing" $+            decodeMaybe @Int32 MySQLNull @?= Right Nothing+        , testCase "TIMESTAMP decodes into LocalTime" $+            decodeLocalTime (MySQLTimeStamp (read "2026-07-21 12:00:00"))+                @?= Right (read "2026-07-21 12:00:00")+        , testCase "nonzero TINYINT decodes as True" $+            decodeBool (MySQLInt8 5) @?= Right True+        , testCase "unsigned TINYINT decodes into Bool" $+            decodeBool (MySQLInt8U 0) @?= Right False+        , testCase "nested Maybe collapses Just Nothing to Nothing (documented NULL lossiness)" $+            fromField @(Maybe (Maybe Int32)) (toField (Just (Nothing :: Maybe Int32)))+                @?= Right Nothing+        , testCase "NaN encodes losslessly (roundtrip == fails only since NaN /= NaN)" $+            fmap isNaN (decodeDouble (encodeDouble (0 / 0))) @?= Right True+        , testCase "lone surrogate in String is rejected by encodeString" $+            encodeString ['\xD800'] @?= Left (EncodeStringLoneSurrogate ['\xD800'])+        ]+    ]
+ test/Integration.hs view
@@ -0,0 +1,46 @@+module Main (main) where++import qualified Data.ByteString as B+import           Database.MySQL.Base+import           System.Environment (lookupEnv)+import           Test.Tasty (defaultMain, testGroup)+import qualified CachingSha2+import qualified MultipleResults+import qualified MysqlTests+import qualified QueryWithoutResultSet+import qualified RoundtripBit+import qualified RoundtripYear+import qualified SelectOne+import qualified TLSConnection+import qualified UnixSocket++main :: IO ()+main = do+    (greet, c) <- connectDetail defaultConnectInfo+        {ciUser = "testMySQLHaskell", ciDatabase = "testMySQLHaskell"}+    close c+    let ver = greetingVersion greet+        isMySql80 = "8." `B.isPrefixOf` ver+                 || "9." `B.isPrefixOf` ver++    -- Probe for a Unix domain socket (MYSQL_UNIX_SOCKET env var, then common paths).+    mSockPath <- UnixSocket.findSocketPath++    -- Probe for TLS CA certificate path (set in NixOS VM tests).+    mTlsCaPath <- lookupEnv "MYSQL_TLS_CA_PATH"++    defaultMain $ testGroup "mysql-integration" $+        [ SelectOne.tests+        , RoundtripBit.tests+        , RoundtripYear.tests+        , MysqlTests.tests+        , QueryWithoutResultSet.tests+        , MultipleResults.tests+        ]+        -- caching_sha2_password is MySQL 8.0+ only (MariaDB does not support it).+        -- The sha2 test users are created by the nix CI config for the MySQL 8.0 VM.+        ++ [ CachingSha2.tests | isMySql80 ]+        -- Unix socket tests are included only when a socket file is found.+        ++ [ UnixSocket.tests p | Just p <- [mSockPath] ]+        -- TLS tests are included only when a CA certificate path is provided.+        ++ [ TLSConnection.tests p | Just p <- [mTlsCaPath] ]
+ test/JSON.hs view
@@ -0,0 +1,32 @@+module JSON where++import Control.Monad+import qualified Data.ByteString as B+import Test.Tasty.HUnit+import Test.Tasty (TestTree)+import System.Directory (getDirectoryContents, doesDirectoryExist)+import System.FilePath ((</>))+import qualified Aeson as A+import qualified Data.Attoparsec.ByteString as A+import qualified AesonBP as B+import qualified Data.Binary.Parser as B+import Data.List+++pathTo :: String -> IO FilePath+pathTo wat = do+  exists <- doesDirectoryExist "test"+  return $ if exists+           then "test" </> wat+           else wat++tests :: IO [TestTree]+tests = do+    path <- pathTo "json-data"+    names <- sort . filter (`notElem` [".", ".."]) <$> getDirectoryContents path+    forM names $ \name -> do+        bs <- B.readFile (path </> name)+        return $ testCase name $+            assertEqual name (A.parseOnly A.jsonEOF' bs) (B.parseOnly B.jsonEOF' bs)++
+ test/Main.hs view
@@ -0,0 +1,35 @@+module Main (main) where++import qualified QC.ByteString as ByteString+import qualified QC.Combinator as Combinator+import Test.Tasty (defaultMain, testGroup)+import qualified JSON+import qualified Sha1Scramble+import qualified Sha256Scramble+import qualified Word24+import qualified TCPStreams+import qualified BoundsCheck+import qualified FieldRoundtrip+import qualified ConnectInfo++main :: IO ()+main = do+    jsonTests <- JSON.tests+    defaultMain $ testGroup "tests" [+      testGroup "binary-parser" [+        testGroup "bs" ByteString.tests+      , testGroup "combinator" Combinator.tests+      , testGroup "JSON" jsonTests+      ],+      testGroup "word24"+         Word24.tests+      , testGroup "tcp-streams"+         [+           TCPStreams.tests+         ]+      , Sha1Scramble.tests+      , Sha256Scramble.tests+      , BoundsCheck.tests+      , FieldRoundtrip.tests+      , ConnectInfo.tests+      ]
+ test/MultipleResults.hs view
@@ -0,0 +1,157 @@+-- | Statements whose reply holds several results: multi-statement strings and+-- CALLs. Whatever a function cannot hand back must still be read off the+-- connection, or the next query receives the previous one's leftovers.+module MultipleResults (tests) where++import Data.Word (Word64)+import           Control.Exception (try)+import           Data.Int          (Int64)+import           Database.MySQL.Base+import qualified System.IO.Streams as Stream+import           System.Timeout    (timeout)+import           Test.Tasty+import           Test.Tasty.HUnit++tests :: TestTree+tests = testGroup "statements with several results"+    [ testCase "query_ with two INSERTs" $ withTable $ \c -> do+        (_, rows) <- query_ c+            "INSERT INTO several_results VALUES (1); INSERT INTO several_results VALUES (2)"+        Stream.skipToEof rows+        assertRowCount c 2+    , testCase "execute_ with two INSERTs" $ withTable $ \c -> do+        _ <- execute_ c+            "INSERT INTO several_results VALUES (1); INSERT INTO several_results VALUES (2)"+        assertRowCount c 2+    , testCase "execute_ with a SELECT" $ withTable $ \c ->+        assertExtraResultSets c (execute_ c "SELECT COUNT(*) FROM several_results")+    , testCase "executeStmt with a SELECT" $ withTable $ \c -> do+        stmt <- prepareStmt c "SELECT COUNT(*) FROM several_results"+        assertExtraResultSets c (executeStmt c stmt [])+    , testCase "execute_ with a CALL returning one result set" $ withTable $ \c -> do+        _ <- execute_ c "DROP PROCEDURE IF EXISTS one_result_set"+        _ <- execute_ c "CREATE PROCEDURE one_result_set() SELECT COUNT(*) FROM several_results"+        assertExtraResultSets c (execute_ c "CALL one_result_set()")+    , testCase "query_ with a CALL returning one result set" $ withTable $ \c -> do+        _ <- execute_ c "DROP PROCEDURE IF EXISTS one_result_set"+        _ <- execute_ c "CREATE PROCEDURE one_result_set() SELECT COUNT(*) FROM several_results"+        (_, rows) <- query_ c "CALL one_result_set()"+        counts <- Stream.toList rows+        assertEqual "rows of the procedure's SELECT" [[MySQLInt64 0]] counts+        assertRowCount c 0+    , testCase "query_ with an INSERT before a SELECT" $ withTable $ \c -> do+        (_, rows) <- query_ c+            "INSERT INTO several_results VALUES (1); SELECT COUNT(*) FROM several_results"+        counts <- Stream.toList rows+        assertEqual "rows of the SELECT" [[MySQLInt64 1]] counts+        assertRowCount c 1+    , testCase "query_ with two SELECTs" $ withTable $ \c -> do+        (_, rows) <- query_ c+            "SELECT COUNT(*) FROM several_results; SELECT COUNT(*) FROM several_results"+        outcome <- try (Stream.toList rows)+        case outcome of+            Left ExtraResultSets -> pure ()+            Right _ -> assertFailure "the second SELECT's result set went unreported"+        again <- Stream.read rows+        assertEqual "reading the finished rows again" Nothing again+        assertRowCount c 0+    , testCase "query_ with a failing statement after a SELECT" $ withTable $ \c -> do+        (_, rows) <- query_ c+            "SELECT COUNT(*) FROM several_results; SELECT * FROM no_such_table"+        assertERRException c (Stream.toList rows)+    , testCase "execute_ with a failing statement after a SELECT" $ withTable $ \c ->+        assertERRException c+            (execute_ c "SELECT COUNT(*) FROM several_results; SELECT * FROM no_such_table")+    , testCase "queryMulti_ with an INSERT and two SELECTs" $ withTable $ \c -> do+        results <- queryMulti_ c+            "INSERT INTO several_results VALUES (1); SELECT COUNT(*) FROM several_results; \+            \SELECT __id FROM several_results"+        assertEqual "every result, in order"+            [Left 1, Right [[MySQLInt64 1]], Right [[MySQLInt32 1]]]+            (map resultShape results)+        assertRowCount c 1+    , testCase "queryMulti_ with a CALL returning two result sets" $ withTable $ \c -> do+        _ <- execute_ c "DROP PROCEDURE IF EXISTS two_result_sets"+        _ <- execute_ c+            "CREATE PROCEDURE two_result_sets() BEGIN \+            \SELECT COUNT(*) FROM several_results; SELECT COUNT(*) + 1 FROM several_results; END"+        results <- queryMulti_ c "CALL two_result_sets()"+        assertEqual "both result sets, then the CALL's OK"+            [Right [[MySQLInt64 0]], Right [[MySQLInt64 1]], Left 0]+            (map resultShape results)+        assertRowCount c 0+    , testCase "queryMulti with a parameter" $ withTable $ \c -> do+        results <- queryMulti c+            "INSERT INTO several_results VALUES (?); SELECT __id FROM several_results"+            [MySQLInt32 7]+        assertEqual "every result, in order"+            [Left 1, Right [[MySQLInt32 7]]]+            (map resultShape results)+    , testCase "queryMulti_ with a failing statement after a SELECT" $ withTable $ \c ->+        assertERRException c+            (queryMulti_ c "SELECT COUNT(*) FROM several_results; SELECT * FROM no_such_table")+    , testCase "executeMany_ with a SELECT after a DO" $ withTable $ \c ->+        assertExtraResultSets c (executeMany_ c "DO 1; SELECT COUNT(*) FROM several_results")+    , testCase "executeMany with a SELECT" $ withTable $ \c ->+        assertExtraResultSets c+            (executeMany c "SELECT COUNT(*) + ? FROM several_results" [[MySQLInt32 1]])+    , testCase "executeMany with two statements per parameter set" $ withTable $ \c -> do+        oks <- executeMany c+            "INSERT INTO several_results VALUES (?); INSERT INTO several_results VALUES (?)"+            [[MySQLInt32 1, MySQLInt32 2], [MySQLInt32 3, MySQLInt32 4]]+        assertEqual "one OK per statement" [1, 1, 1, 1] (map okAffectedRows oks)+        assertRowCount c 4+    , testCase "executeMany with no parameter sets" $ withTable $ \c -> do+        oks <- executeMany c "INSERT INTO several_results VALUES (?)" ([] :: [[MySQLValue]])+        assertEqual "no OKs" [] (map okAffectedRows oks)+        assertRowCount c 0+    ]++-- | The affected rows of an 'OK', or the rows of a result set.+resultShape :: StatementResult -> Either Word64 [[MySQLValue]]+resultShape result = case result of+    StatementOK ok -> Left (okAffectedRows ok)+    StatementRows _ rows -> Right rows++-- | Generous for a few statements on an empty temporary table, and short enough+-- that a hang fails the suite instead of stalling CI.+replyTimeLimitMicroseconds :: Int+replyTimeLimitMicroseconds = 10000000++-- | Runs a case on a fresh connection with an empty temporary table.+withTable :: (MySQLConn -> Assertion) -> Assertion+withTable body = do+    (_, c) <- connectDetail defaultConnectInfo+        { ciUser = "testMySQLHaskell"+        , ciDatabase = "testMySQLHaskell"+        }+    _ <- execute_ c "CREATE TEMPORARY TABLE several_results (__id INT)"+    finished <- timeout replyTimeLimitMicroseconds (body c)+    case finished of+        Nothing -> assertFailure "blocked for 10 s waiting for a reply"+        Just () -> close c++-- | The statement must raise 'ExtraResultSets' and leave the connection in step.+assertExtraResultSets :: MySQLConn -> IO a -> Assertion+assertExtraResultSets c runStatement = do+    outcome <- try runStatement+    case outcome of+        Left ExtraResultSets -> pure ()+        Right _ -> assertFailure "the result set went unreported"+    assertRowCount c 0++-- | The failing statement's error must come from this call, not the next one.+assertERRException :: MySQLConn -> IO a -> Assertion+assertERRException c runStatement = do+    outcome <- try runStatement+    case outcome of+        Left (ERRException _) -> pure ()+        Right _ -> assertFailure "the failing statement's error went unreported"+    assertRowCount c 0++-- | The connection must be back in step: a fresh query gets its own rows.+assertRowCount :: MySQLConn -> Int64 -> Assertion+assertRowCount c expected = do+    (_, rows) <- query_ c "SELECT COUNT(*) FROM several_results"+    counts <- Stream.toList rows+    assertEqual "rows of the next query" [[MySQLInt64 expected]] counts
+ test/MysqlTests.hs view
@@ -0,0 +1,187 @@+{-# LANGUAGE ScopedTypeVariables #-}++module MysqlTests(tests)  where++import qualified BinaryRow+import qualified BinaryRowNew+import qualified BinLog+import qualified BinLogNew+import           Control.Concurrent    (forkIO, threadDelay)+import           Control.Exception     (bracket, catch)+import           Control.Monad+import qualified Data.ByteString       as B+import           Database.MySQL.Base+import           Database.MySQL.BinLog+import           System.Environment+import qualified System.IO.Streams as Stream+import           Test.Tasty+import           Test.Tasty.HUnit+import qualified TextRow+import qualified ExecuteMany+import qualified TextRowNew++tests :: TestTree+tests = testCaseSteps "mysql-haskell test suit" $ \step -> do++    step "preparing table..."+    (greet, c) <- connectDetail defaultConnectInfo {ciUser = "testMySQLHaskell", ciDatabase = "testMySQLHaskell"}++    let ver = greetingVersion greet+        isOld = "5.0" `B.isPrefixOf` ver+                || "5.1" `B.isPrefixOf` ver+                || "5.5" `B.isPrefixOf` ver+        isNew = not isOld  -- MySQL 5.6+ and MariaDB 10+ support fractional seconds+                           -- in TIME, DATETIME, and TIMESTAMP columns+++    execute_ c "DROP TABLE IF EXISTS test"+    execute_ c "DROP TABLE IF EXISTS test_new"++    execute_ c  "CREATE TABLE test(\+                \__id           INT,\+                \__bit          BIT(16),\+                \__tinyInt      TINYINT,\+                \__tinyIntU     TINYINT UNSIGNED,\+                \__smallInt     SMALLINT,\+                \__smallIntU    SMALLINT UNSIGNED,\+                \__mediumInt    MEDIUMINT,\+                \__mediumIntU   MEDIUMINT UNSIGNED,\+                \__int          INT,\+                \__intU         INT UNSIGNED,\+                \__bigInt       BIGINT,\+                \__bigIntU      BIGINT UNSIGNED,\+                \__decimal      DECIMAL(20,10),\+                \__float        FLOAT,\+                \__double       DOUBLE,\+                \__date         DATE,\+                \__datetime     DATETIME,\+                \__timestamp    TIMESTAMP NULL,\+                \__time         TIME,\+                \__year         YEAR(4),\+                \__char         CHAR(8),\+                \__varchar      VARCHAR(1024),\+                \__binary       BINARY(8),\+                \__varbinary    VARBINARY(1024),\+                \__tinyblob     TINYBLOB,\+                \__tinytext     TINYTEXT,\+                \__blob         BLOB(1000000),\+                \__text         TEXT(1000000),\+                \__enum         ENUM('foo', 'bar', 'qux'),\+                \__set          SET('foo', 'bar', 'qux')\+                \) CHARACTER SET utf8"++    resetTestTable c++    step "testing executeMany"+    ExecuteMany.tests c++    resetTestTable c++    step "testing text protocol"+    TextRow.tests c++    resetTestTable c++    step "testing binary protocol"+    BinaryRow.tests c++    resetTestTable c+++    when isNew $ do+        execute_ c "CREATE TABLE test_new(\+                   \__id           INT,\+                   \__datetime     DATETIME(2),\+                   \__timestamp    TIMESTAMP(4) NULL,\+                   \__time         TIME(6)\+                   \) CHARACTER SET utf8"++        resetTest57Table c++        step "testing MySQL5.7 extra text protocol"+        TextRowNew.tests c++        resetTest57Table c++        step "testing MySQL5.7 extra binary protocol"+        BinaryRowNew.tests c++        void $ resetTest57Table c++    step "testing binlog protocol"++    if isNew+    then do+        forkIO BinLogNew.eventProducer+        BinLogNew.tests c+    else do+        forkIO BinLog.eventProducer+        BinLog.tests c++    close c++    step "testing password change"+    (_, c) <- connectDetail defaultConnectInfo {ciUser = "testMySQLHaskell", ciDatabase = "testMySQLHaskell"}+    -- ALTER USER works on both MariaDB and MySQL 8.0 (SET PASSWORD = PASSWORD('...') was removed in MySQL 8.0)+    execute_ c "ALTER USER 'testMySQLHaskell'@'localhost' IDENTIFIED BY '123456abcdefg???'"+    close c++    (_, c) <- connectDetail+        defaultConnectInfo {ciUser = "testMySQLHaskell", ciDatabase = "testMySQLHaskell", ciPassword = "123456abcdefg???"}+    execute_ c "ALTER USER 'testMySQLHaskell'@'localhost' IDENTIFIED BY ''"+    close c++    catch+        (void $ connectDetail+                    defaultConnectInfo+                        {ciUser = "testMySQLHaskell", ciDatabase = "testMySQLHaskell", ciPassword = "wrongPassWord"})+        (\ (e :: ERRException) -> do+            let ERRException err = e+            assertEqual "wrong password should fail with error 1045" 1045 (errCode err))++  where+    resetTestTable c = do+            execute_ c  "DELETE FROM test WHERE __id=0"+            execute_ c  "INSERT INTO test VALUES(\+                    \0,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL,\+                    \NULL\+                    \)"++    resetTest57Table c = do+            execute_ c  "DELETE FROM test_new WHERE __id=0"+            execute_ c  "INSERT INTO test_new VALUES(\+                        \0,\+                        \NULL,\+                        \NULL,\+                        \NULL\+                        \)"++
+ test/Orphans.hs view
@@ -0,0 +1,25 @@+{-# OPTIONS_GHC -Wno-orphans #-}++-- https://tech.freckle.com/2018/12/12/a-home-for-orphan-instances/+module Orphans where++import Test.QuickCheck++import Data.Int.Int24+import Data.Word.Word24++-- Arbitrary/CoArbitrary instances for Int24 and Word24++instance Arbitrary Int24 where+  arbitrary = arbitraryBoundedIntegral+  shrink = shrinkIntegral++instance CoArbitrary Int24 where+  coarbitrary = coarbitraryIntegral++instance Arbitrary Word24 where+  arbitrary = arbitraryBoundedIntegral+  shrink = shrinkIntegral++instance CoArbitrary Word24 where+  coarbitrary = coarbitraryIntegral
+ test/QC/ByteString.hs view
@@ -0,0 +1,175 @@+{-# LANGUAGE BangPatterns, CPP, OverloadedStrings #-}+module QC.ByteString (tests) where++import Data.Char (chr, ord)+import Data.Int (Int64)+import Data.Word (Word8)+import Prelude hiding (take, takeWhile)+import QC.Common (liftOp, parseBS, toStrictBS)+import Test.Tasty (TestTree)+import Test.Tasty.QuickCheck (testProperty)+import Test.QuickCheck+import qualified Data.Scientific as Sci+import qualified Data.ByteString.Builder.Scientific as Sci+import qualified Data.Binary.Parser as P+import qualified Data.ByteString as B+import qualified Data.ByteString.Builder as BB+import qualified Data.ByteString.Lazy as L+import qualified Data.ByteString.Lazy.Char8 as L8++newtype ASCIIChar = ASCIIChar Char+    deriving (Eq, Ord, Show, Read)+instance Arbitrary ASCIIChar where+    arbitrary = ASCIIChar <$> choose ('\0', '\127')+    shrink (ASCIIChar c) = ASCIIChar <$> shrink c++-- Basic byte-level combinators.++satisfy :: Word8 -> L.ByteString -> Property+satisfy w s = parseBS (P.satisfy (<=w)) (L.cons w s) === Just w++satisfyWith :: ASCIIChar -> L.ByteString -> Property+satisfyWith (ASCIIChar c) s = parseBS (P.satisfyWith (chr . fromIntegral) (<=c))+                         (L.cons (fromIntegral (ord c)) s) === Just c++word8 :: Word8 -> L.ByteString -> Property+word8 w s = parseBS (P.word8 w *> pure w) (L.cons w s) === Just w++skipWord8 :: Word8 -> L.ByteString -> Property+skipWord8 w s =+  case (parseBS (P.skipWord8 (<w)) s, L.uncons s) of+    (Nothing, mcs) -> maybe (property True) (expectFailure . it) mcs+    (Just _,  mcs) -> maybe (property False) it mcs+  where it cs = liftOp "<" (<) (fst cs) w++anyWord8 :: L.ByteString -> Property+anyWord8 s+    | L.null s  = p === Nothing+    | otherwise = p === Just (L.head s)+  where p = parseBS P.anyWord8 s++peekMaybe :: L.ByteString -> Property+peekMaybe s+    | L.null s  = p === Just (Nothing, s)+    | otherwise = p === Just (Just (L.head s), s)+  where p = parseBS ((,) <$> P.peekMaybe <*> P.getRemainingLazyByteString) s++peek :: L.ByteString -> Property+peek s = parseBS P.peek s === (fst <$> L.uncons s)++string :: L.ByteString -> L.ByteString -> Property+string s t = parseBS (P.string s' *> pure s') (s `L.append` t) === Just s'+  where s' = toStrictBS s++strings :: L.ByteString -> L.ByteString -> L.ByteString -> Property+strings s t u =+    parseBS (P.string (toStrictBS s) >> (P.string t' *> pure t')) (L.concat [s,t,u])+    === Just t'+  where t' = toStrictBS t++skipWhile :: Word8 -> L.ByteString -> Property+skipWhile w s =+    let t = L.dropWhile (<=w) s+    in case P.runGetOrFail (P.skipWhile (<=w)) s of+         Right (t', _, _) -> t === t'+         Left  _          -> property False++takeCount :: Positive Int -> L.ByteString -> Property+takeCount (Positive k) s =+    case parseBS (P.getByteString k) s of+      Nothing -> liftOp ">" (>) (fromIntegral k) (L.length s)+      Just _s -> liftOp "<=" (<=) (fromIntegral k) (L.length s)++takeWhile :: Word8 -> L.ByteString -> Property+takeWhile w s =+    let (h,t) = L.span (==w) s+    in case P.runGetOrFail (P.takeWhile (==w)) s of+         Right (t', _, h') -> t === t' .&&. toStrictBS h === h'+         Left _            -> property False++take :: Int -> L.ByteString -> Property+take n s = maybe (property $ L.length s < fromIntegral n)+           (=== B.take n (toStrictBS s)) $+           parseBS (P.getByteString n) s++remaining :: L.ByteString -> Property+remaining s = maybe (property False) (=== s) .+                       parseBS P.getRemainingLazyByteString $ s++takeWhile1 :: Word8 -> L.ByteString -> Property+takeWhile1 w s =+    let s'    = L.cons w s+        (h,t) = L.span (<=w) s'+    in case P.runGetOrFail (P.takeWhile1 (<=w)) s' of+         Right (t', _, h') -> t === t' .&&. toStrictBS h === h'+         _                 -> property False++takeTill :: Word8 -> L.ByteString -> Property+takeTill w s =+    let (h,t) = L.break (==w) s+    in case P.runGetOrFail (P.takeTill (==w)) s of+         Right (t', _, h') -> t === t' .&&. toStrictBS h === h'+         _                 -> property False++takeWhile1_empty :: Property+takeWhile1_empty = parseBS (P.takeWhile1 undefined) L.empty === Nothing++endOfInput :: L.ByteString -> Property+endOfInput s = parseBS P.endOfInput s === if L.null s+                                          then Just ()+                                          else Nothing++endOfLine :: L.ByteString -> Property+endOfLine s =+  case (parseBS P.endOfLine s, L8.uncons s) of+    (Nothing, mcs) -> maybe (property True) (expectFailure . eol) mcs+    (Just _,  mcs) -> maybe (property False) eol mcs+  where eol (c,s') = c === '\n' .||.+                     (c, fst <$> L8.uncons s') === ('\r', Just '\n')++scan :: L.ByteString -> Positive Int64 -> Property+scan s (Positive k) = parseBS p s === Just (toStrictBS $ L.take k s)+  where p = P.scan k $ \ n _ ->+            if n > 0 then let !n' = n - 1 in Just n' else Nothing++decimal :: Integer -> Property+decimal d =+    let dBS = BB.toLazyByteString $ BB.integerDec d+    in parseBS (P.signed P.decimal) dBS === Just d++double :: Double -> Property+double d =+    let dBS = BB.toLazyByteString $ BB.doubleDec d+    in parseBS P.double dBS === Just d++scientific :: Sci.Scientific -> Property+scientific sci =+    let sciBS = BB.toLazyByteString $ Sci.formatScientificBuilder Sci.Generic Nothing sci+    in parseBS P.scientific sciBS === Just sci++tests :: [TestTree]+tests = [+      testProperty "anyWord8" anyWord8+    , testProperty "endOfInput" endOfInput+    , testProperty "endOfLine" endOfLine+    , testProperty "peekMaybe" peekMaybe+    , testProperty "peek" peek+    , testProperty "satisfy" satisfy+    , testProperty "satisfyWith" satisfyWith+    , testProperty "scan" scan+    , testProperty "skipWord8" skipWord8+    , testProperty "skipWhile" skipWhile+    , testProperty "string" string+    , testProperty "strings" strings+    , testProperty "take" take+    , testProperty "takeCount" takeCount+    , testProperty "remaining" remaining+    , testProperty "takeTill" takeTill+    , testProperty "takeWhile" takeWhile+    , testProperty "takeWhile1" takeWhile1+    , testProperty "takeWhile1_empty" takeWhile1_empty+    , testProperty "word8" word8+    , testProperty "decimal" decimal+    , testProperty "double" double+    , testProperty "scientific" scientific+  ]
+ test/QC/Combinator.hs view
@@ -0,0 +1,39 @@+{-# LANGUAGE CPP, OverloadedStrings #-}++module QC.Combinator where++import qualified Control.Monad as M (replicateM)+import Data.Maybe (fromJust, isJust)+import Data.Word (Word8)+import QC.Common (Repack, parseBS, repackBS, toLazyBS)+import Test.Tasty (TestTree)+import Test.Tasty.QuickCheck (testProperty)+import Test.QuickCheck+import qualified Data.Binary.Parser as P+import qualified Data.ByteString as B+import qualified Data.Foldable as Foldable (asum)++asum :: NonEmptyList (NonEmptyList Word8) -> Gen Property+asum (NonEmpty xs) = do+  let ys = map (B.pack . getNonEmpty) xs+  return . forAll (repackBS <$> arbitrary <*> elements ys) $+    maybe False (`elem` ys) . parseBS (Foldable.asum (map (\s -> P.string s *> pure s) ys))++replicateM :: Positive (Small Int) -> Repack -> B.ByteString -> Bool+replicateM (Positive (Small n)) rs s =+    (length <$> parseBS (M.replicateM n (P.string s)) input) == Just n+  where input = repackBS rs (B.concat (replicate (n+1) s))++lookAhead :: NonEmptyList Word8 -> Bool+lookAhead (NonEmpty xs) =+  let ys = B.pack xs+      withLookAheadThenConsume = (\x y -> (x, y)) <$> P.lookAhead (P.string ys) <*> P.string ys+      mr = parseBS withLookAheadThenConsume $ toLazyBS ys+  in isJust mr && fst (fromJust mr) == snd (fromJust mr)++tests :: [TestTree]+tests = [+    testProperty "asum" asum+  , testProperty "replicateM" replicateM+  , testProperty "lookAhead" lookAhead+  ]
+ test/QC/Common.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE CPP, FlexibleInstances #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}+module QC.Common+    (+      ASCII(..)+    , parseBS+    , toLazyBS+    , toStrictBS+    , Repack+    , repackBS+    , repackBS_+    , liftOp+    ) where++import Data.Char (isAlpha)+import Test.QuickCheck+import Test.QuickCheck.Instances ()+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import qualified Data.Binary.Parser as P++parseBS :: P.Get r -> BL.ByteString -> Maybe r+parseBS p lbs = case P.parseLazy p lbs of+    Left _ -> Nothing+    Right v -> Just v++toStrictBS :: BL.ByteString -> B.ByteString+toStrictBS = B.concat . BL.toChunks++toLazyBS :: B.ByteString -> BL.ByteString+toLazyBS = BL.fromChunks . (:[])++newtype ASCII a = ASCII { fromASCII :: a }+                  deriving (Eq, Ord, Show)++instance Arbitrary (ASCII B.ByteString) where+    arbitrary = (ASCII . B.pack) <$> listOf (choose (0,127))+    shrink = map (ASCII . B.pack) . shrink . B.unpack . fromASCII++instance Arbitrary (ASCII BL.ByteString) where+    arbitrary = ASCII <$> (repackBS <$> arbitrary <*> (fromASCII <$> arbitrary))+    shrink = map (ASCII . BL.pack) . shrink . BL.unpack . fromASCII++type Repack = NonEmptyList (Positive (Small Int))++repackBS :: Repack -> B.ByteString -> BL.ByteString+repackBS (NonEmpty bs) =+    BL.fromChunks . repackBS_ (map (getSmall . getPositive) bs)++repackBS_ :: [Int] -> B.ByteString -> [B.ByteString]+repackBS_ = go . cycle+  where go (b:bs) s+          | B.null s = []+          | otherwise = let (h,t) = B.splitAt b s+                        in h : go bs t+        go _ _ = error "unpossible"++liftOp :: (Show a, Testable prop) =>+          String -> (a -> a -> prop) -> a -> a -> Property+liftOp name f x y = counterexample desc (f x y)+  where op = case name of+               (c:_) | isAlpha c -> " `" ++ name ++ "` "+                     | otherwise -> " " ++ name ++ " "+               _ -> " ??? "+        desc = "not (" ++ show x ++ op ++ show y ++ ")"
+ test/QueryWithoutResultSet.hs view
@@ -0,0 +1,60 @@+-- | Statements without a result set (INSERT, UPDATE, DELETE) sent through the+-- functions that expect one. The server answers with a lone OK packet, which+-- used to leave these functions blocked forever waiting for column+-- definitions that never come (issue #47).+module QueryWithoutResultSet (tests) where++import           Database.MySQL.Base+import qualified Data.Vector       as V+import qualified System.IO.Streams as Stream+import           System.Timeout    (timeout)+import           Test.Tasty+import           Test.Tasty.HUnit++tests :: TestTree+tests = testGroup "query functions given an INSERT"+    [ testCase "query_" $ assertEmptyResultSet $ \c -> do+        (columns, rows) <- query_ c "INSERT INTO without_result_set VALUES (1)"+        rowList <- Stream.toList rows+        pure (length columns, length rowList)+    , testCase "queryVector_" $ assertEmptyResultSet $ \c -> do+        (columns, rows) <- queryVector_ c "INSERT INTO without_result_set VALUES (1)"+        rowList <- Stream.toList rows+        pure (V.length columns, length rowList)+    , testCase "queryStmt" $ assertEmptyResultSet $ \c -> do+        stmt <- prepareStmt c "INSERT INTO without_result_set VALUES (?)"+        (columns, rows) <- queryStmt c stmt [MySQLInt32 1]+        rowList <- Stream.toList rows+        pure (length columns, length rowList)+    , testCase "queryStmtVector" $ assertEmptyResultSet $ \c -> do+        stmt <- prepareStmt c "INSERT INTO without_result_set VALUES (?)"+        (columns, rows) <- queryStmtVector c stmt [MySQLInt32 1]+        rowList <- Stream.toList rows+        pure (V.length columns, length rowList)+    ]++-- | Generous for one INSERT into an empty temporary table, and short enough+-- that a hang fails the suite instead of stalling CI.+replyTimeLimitMicroseconds :: Int+replyTimeLimitMicroseconds = 10000000++-- | Runs the insert on a fresh connection; it reports the column and row counts+-- of the result set it got back. Both must be zero, the row must be inserted+-- exactly once, and the connection must stay usable afterwards.+assertEmptyResultSet :: (MySQLConn -> IO (Int, Int)) -> Assertion+assertEmptyResultSet runInsert = do+    (_, c) <- connectDetail defaultConnectInfo+        { ciUser = "testMySQLHaskell"+        , ciDatabase = "testMySQLHaskell"+        }+    _ <- execute_ c "CREATE TEMPORARY TABLE without_result_set (__id INT)"+    outcome <- timeout replyTimeLimitMicroseconds (runInsert c)+    case outcome of+        Nothing -> assertFailure+            "blocked for 10 s waiting for a result set the server never sends"+        Just columnsAndRows -> do+            assertEqual "columns and rows returned" (0, 0) columnsAndRows+            (_, rows) <- query_ c "SELECT COUNT(*) FROM without_result_set"+            counts <- Stream.toList rows+            assertEqual "the row was inserted once" [[MySQLInt64 1]] counts+    close c
+ test/RoundtripBit.hs view
@@ -0,0 +1,46 @@+module RoundtripBit (tests) where++import Database.MySQL.Base+import qualified System.IO.Streams as Stream+import Test.Tasty+import Test.Tasty.HUnit++tests :: TestTree+tests = testCaseSteps "roundtrip MySQLBit" $ \step -> do+    (_, c) <- connectDetail defaultConnectInfo+        { ciUser = "testMySQLHaskell"+        , ciDatabase = "testMySQLHaskell"+        }++    execute_ c "CREATE TEMPORARY TABLE test_bit (__id INT, __val BIT(16))"+    execute_ c "INSERT INTO test_bit VALUES (1, NULL)"++    step "roundtrip MySQLBit via binary protocol"+    updStmt <- prepareStmt c "UPDATE test_bit SET __val = ? WHERE __id = 1"+    selStmt <- prepareStmt c "SELECT __val FROM test_bit WHERE __id = 1"++    let bitVal = 43744 -- 0b1010101011100000+    executeStmt c updStmt [MySQLBit bitVal]++    (_, is) <- queryStmt c selStmt []+    Just [v] <- Stream.read is+    Stream.skipToEof is+    assertEqual "MySQLBit roundtrips through BIT(16)" (MySQLBit bitVal) v++    step "roundtrip MySQLBit zero"+    executeStmt c updStmt [MySQLBit 0]++    (_, is2) <- queryStmt c selStmt []+    Just [v2] <- Stream.read is2+    Stream.skipToEof is2+    assertEqual "MySQLBit 0 roundtrips" (MySQLBit 0) v2++    step "roundtrip MySQLBit max for BIT(16)"+    executeStmt c updStmt [MySQLBit 65535]++    (_, is3) <- queryStmt c selStmt []+    Just [v3] <- Stream.read is3+    Stream.skipToEof is3+    assertEqual "MySQLBit 65535 roundtrips" (MySQLBit 65535) v3++    close c
+ test/RoundtripYear.hs view
@@ -0,0 +1,53 @@+module RoundtripYear (tests) where++import Database.MySQL.Base+import qualified System.IO.Streams as Stream+import Test.Tasty+import Test.Tasty.HUnit++tests :: TestTree+tests = testCaseSteps "roundtrip MySQLYear" $ \step -> do+    (_, c) <- connectDetail defaultConnectInfo+        { ciUser = "testMySQLHaskell"+        , ciDatabase = "testMySQLHaskell"+        }++    execute_ c "CREATE TEMPORARY TABLE test_year (__id INT, __val YEAR(4))"+    execute_ c "INSERT INTO test_year VALUES (1, NULL)"++    step "roundtrip MySQLYear via binary protocol"+    updStmt <- prepareStmt c "UPDATE test_year SET __val = ? WHERE __id = 1"+    selStmt <- prepareStmt c "SELECT __val FROM test_year WHERE __id = 1"++    executeStmt c updStmt [MySQLYear 1999]++    (_, is) <- queryStmt c selStmt []+    Just [v] <- Stream.read is+    Stream.skipToEof is+    assertEqual "MySQLYear 1999 roundtrips" (MySQLYear 1999) v++    step "roundtrip MySQLYear min (1901)"+    executeStmt c updStmt [MySQLYear 1901]++    (_, is2) <- queryStmt c selStmt []+    Just [v2] <- Stream.read is2+    Stream.skipToEof is2+    assertEqual "MySQLYear 1901 roundtrips" (MySQLYear 1901) v2++    step "roundtrip MySQLYear max (2155)"+    executeStmt c updStmt [MySQLYear 2155]++    (_, is3) <- queryStmt c selStmt []+    Just [v3] <- Stream.read is3+    Stream.skipToEof is3+    assertEqual "MySQLYear 2155 roundtrips" (MySQLYear 2155) v3++    step "roundtrip MySQLYear zero"+    executeStmt c updStmt [MySQLYear 0]++    (_, is4) <- queryStmt c selStmt []+    Just [v4] <- Stream.read is4+    Stream.skipToEof is4+    assertEqual "MySQLYear 0 roundtrips" (MySQLYear 0) v4++    close c
+ test/SelectOne.hs view
@@ -0,0 +1,25 @@+{-# LANGUAGE ScopedTypeVariables #-}++module SelectOne (tests) where++import Database.MySQL.Base+import qualified System.IO.Streams as Stream+import Test.Tasty+import Test.Tasty.HUnit++tests :: TestTree+tests = testCaseSteps "select 1" $ \step -> do+    step "connecting..."+    (_, c) <- connectDetail defaultConnectInfo+        { ciUser = "testMySQLHaskell"+        , ciDatabase = "testMySQLHaskell"+        }++    step "executing SELECT 1..."+    (_, is) <- query_ c "SELECT 1"+    Just row <- Stream.read is+    -- MySQL 8.0 returns MySQLInt64 for integer literals, older versions return MySQLInt32+    assertBool "SELECT 1 returns 1" (row == [MySQLInt32 1] || row == [MySQLInt64 1])+    Stream.skipToEof is++    close c
+ test/Sha1Scramble.hs view
@@ -0,0 +1,49 @@+module Sha1Scramble (tests) where++import qualified Data.ByteString as B+import qualified Data.ByteString.Base16 as Base16+import           Data.ByteString.Char8 (pack)+import           Database.MySQL.Connection (scrambleSHA1)+import           Test.Tasty+import           Test.Tasty.HUnit++hexToBytes :: String -> B.ByteString+hexToBytes = Base16.decodeLenient . pack++tests :: TestTree+tests = testGroup "SHA1 Scramble"+    [ testCase "empty password returns empty" $ do+        let result = scrambleSHA1 "some_salt" ""+        assertEqual "empty password" B.empty result++    , testCase "non-empty password returns 20 bytes" $ do+        let result = scrambleSHA1 "12345678901234567890" "password"+        assertEqual "scramble length" 20 (B.length result)++    , testCase "different salts produce different scrambles" $ do+        let r1 = scrambleSHA1 "salt1_______________" "password"+            r2 = scrambleSHA1 "salt2_______________" "password"+        assertBool "different salts should produce different results" (r1 /= r2)++    , testCase "different passwords produce different scrambles" $ do+        let salt = "12345678901234567890"+            r1 = scrambleSHA1 salt "password1"+            r2 = scrambleSHA1 salt "password2"+        assertBool "different passwords should produce different results" (r1 /= r2)++    , testCase "scramble is deterministic" $ do+        let salt = "12345678901234567890"+            r1 = scrambleSHA1 salt "testPassword123"+            r2 = scrambleSHA1 salt "testPassword123"+        assertEqual "same inputs produce same output" r1 r2++    , testCase "golden: salt=12345678901234567890 pass=password" $ do+        let result = scrambleSHA1 "12345678901234567890" "password"+            expected = hexToBytes "1957dce2724282e018f40d905824cb6361f88d41"+        assertEqual "golden vector 1" expected result++    , testCase "golden: salt=ABCDEFGHIJKLMNOPQRST pass=secret" $ do+        let result = scrambleSHA1 "ABCDEFGHIJKLMNOPQRST" "secret"+            expected = hexToBytes "28441590674285e7d03cae7af237504797f70e91"+        assertEqual "golden vector 2" expected result+    ]
+ test/Sha256Scramble.hs view
@@ -0,0 +1,49 @@+module Sha256Scramble (tests) where++import qualified Data.ByteString as B+import qualified Data.ByteString.Base16 as Base16+import           Data.ByteString.Char8 (pack)+import           Database.MySQL.Connection (scrambleSHA256)+import           Test.Tasty+import           Test.Tasty.HUnit++hexToBytes :: String -> B.ByteString+hexToBytes = Base16.decodeLenient . pack++tests :: TestTree+tests = testGroup "SHA256 Scramble"+    [ testCase "empty password returns empty" $ do+        let result = scrambleSHA256 "some_salt" ""+        assertEqual "empty password" B.empty result++    , testCase "non-empty password returns 32 bytes" $ do+        let result = scrambleSHA256 "12345678901234567890" "password"+        assertEqual "scramble length" 32 (B.length result)++    , testCase "different salts produce different scrambles" $ do+        let r1 = scrambleSHA256 "salt1_______________" "password"+            r2 = scrambleSHA256 "salt2_______________" "password"+        assertBool "different salts should produce different results" (r1 /= r2)++    , testCase "different passwords produce different scrambles" $ do+        let salt = "12345678901234567890"+            r1 = scrambleSHA256 salt "password1"+            r2 = scrambleSHA256 salt "password2"+        assertBool "different passwords should produce different results" (r1 /= r2)++    , testCase "scramble is deterministic" $ do+        let salt = "12345678901234567890"+            r1 = scrambleSHA256 salt "testPassword123"+            r2 = scrambleSHA256 salt "testPassword123"+        assertEqual "same inputs produce same output" r1 r2++    , testCase "golden: salt=12345678901234567890 pass=password" $ do+        let result = scrambleSHA256 "12345678901234567890" "password"+            expected = hexToBytes "718d80fb92b693f7ee1fcad95bfe7a2be1f332ac2c3ec6cdc85893b65b240650"+        assertEqual "golden vector 1" expected result++    , testCase "golden: salt=ABCDEFGHIJKLMNOPQRST pass=secret" $ do+        let result = scrambleSHA256 "ABCDEFGHIJKLMNOPQRST" "secret"+            expected = hexToBytes "d721e183c1f036a196c1389201b8d53f38064a16f1d0923683ab886763152d75"+        assertEqual "golden vector 2" expected result+    ]
+ test/TCPStreams.hs view
@@ -0,0 +1,122 @@+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE ScopedTypeVariables #-}++module TCPStreams (tests) where++------------------------------------------------------------------------------+import           Control.Concurrent             (forkIO, newEmptyMVar, putMVar,+                                                 takeMVar)+import qualified Network.Socket                 as N+import           System.Timeout                 (timeout)+import           Test.Tasty+import           Test.Tasty.HUnit+import qualified Data.ByteString                as B+import qualified Data.ByteString.Lazy           as L+------------------------------------------------------------------------------+import qualified Data.TLSSetting                as TLS+import           Data.Connection+import qualified System.IO.Streams              as Stream+import qualified System.IO.Streams.TCP          as TCP+import qualified System.IO.Streams.TLS          as TLS+------------------------------------------------------------------------------++import Test.Tasty.ExpectedFailure (ignoreTestBecause)++tests :: TestTree+tests = testGroup "tests" [ testGroup "TCP" tcpTests+        , testGroup "TLS"  tlsTests+        , testGroup "HTTPS" [testHTTPS]+        ]++------------------------------------------------------------------------------++tcpTests :: [TestTree]+tcpTests = [ testTCPSocket ]++testTCPSocket :: TestTree+testTCPSocket = testCase "network/socket" $+    N.withSocketsDo $ do+    x <- timeout (10 * 10^(6::Int)) go+    assertEqual "ok" (Just ()) x++  where+    go = do+        portMVar   <- newEmptyMVar+        resultMVar <- newEmptyMVar+        _ <- forkIO $ client portMVar resultMVar+        server portMVar+        l <- takeMVar resultMVar+        assertEqual "testSocket" l ["ok"]++    client mvar resultMVar = do+        _ <- takeMVar mvar+        conn <- TCP.connect "127.0.0.1" 8888+        send conn "ok"+        Stream.toList (source conn) >>= putMVar resultMVar+        close conn++    server mvar = do+        sock <- TCP.bindAndListen 1024 8888+        putMVar mvar ()+        conn <- TCP.accept sock+        req <- Stream.readExactly 2 (source conn)+        send conn (L.fromStrict req)+        close conn++------------------------------------------------------------------------------++tlsTests :: [TestTree]+tlsTests = [ testTLSSocket+             -- TODO disabled because won't work in a  nix container+             -- , testHTTPS+           ]++testTLSSocket :: TestTree+testTLSSocket = testCase "network/socket" $+    N.withSocketsDo $ do+    x <- timeout (10 * 10^(6::Int)) go+    assertEqual "ok" (Just ()) x++  where+    go = do+        portMVar   <- newEmptyMVar+        resultMVar <- newEmptyMVar+        _ <- forkIO $ client portMVar resultMVar+        server portMVar+        l <- takeMVar resultMVar+        assertEqual "testSocket" l ["ok"]++    client mvar resultMVar = do+        _ <- takeMVar mvar+        cp <- TLS.makeClientParams (TLS.CustomCAStore "./test/cert/ca.pem")+        conn <- TLS.connect cp (Just "Winter") "127.0.0.1" 8889+        send conn "ok"+        Stream.toList (source conn) >>= putMVar resultMVar+        close conn++    server mvar = do+        sp <- TLS.makeServerParams "./test/cert/server-cert.pem" [] "./test/cert/server-key.pem"+        sock <- TCP.bindAndListen 1024 8889+        putMVar mvar ()+        conn <- TLS.accept sp sock+        req <- Stream.readExactly 2 (source conn)+        send conn (L.fromStrict req)+        close conn++testHTTPS :: TestTree+testHTTPS =+  ignoreTestBecause "can't connect to socket in a nix container, the build becomes no longer reproducible." $+  testCase "network/https" $+    N.withSocketsDo $ do+    x <- timeout (10 * 10^(6::Int)) go+    assertEqual "ok" (Just 1024) x+  where+    go = do+        cp <- TLS.makeClientParams TLS.MozillaCAStore+        conn <- TLS.connect cp Nothing "www.bing.com" 443+        send conn ("GET / HTTP/1.1\r\n")+        send conn ("Host: www.bing.com\r\n")+        send conn ("\r\n")+        bs <- Stream.readExactly 1024 (source conn)+        close conn+        return (B.length bs)
+ test/TLSConnection.hs view
@@ -0,0 +1,76 @@+{-# LANGUAGE ScopedTypeVariables #-}++module TLSConnection (tests) where++import qualified Data.ByteString       as B+import qualified Data.Text             as T+import           Database.MySQL.Base+import qualified Database.MySQL.TLS    as TLS+import           Database.MySQL.TLS    (makeClientParams, TrustedCAStore(..))+import qualified System.IO.Streams     as Stream+import           Test.Tasty+import           Test.Tasty.HUnit++tests :: FilePath -> TestTree+tests caPath = testGroup "tls-connection"+    [ testCaseSteps "TLS connectDetail: SELECT 1" $ \step -> do+        step "creating TLS client params..."+        cparams <- makeClientParams (CustomCAStore caPath)++        step "connecting via TLS..."+        (greet, conn) <- TLS.connectDetail+            defaultConnectInfo { ciUser = "testMySQLHaskell", ciDatabase = "testMySQLHaskell" }+            (cparams, "Winter")++        step "checking greeting version..."+        let ver = greetingVersion greet+        assertBool "greeting version is not empty" (not $ B.null ver)++        step "executing SELECT 1..."+        (_, is) <- query_ conn "SELECT 1"+        Just row <- Stream.read is+        assertBool "SELECT 1 returns 1"+            (row == [MySQLInt32 1] || row == [MySQLInt64 1])+        Stream.skipToEof is++        close conn++    , testCaseSteps "TLS connection: verify encryption active" $ \step -> do+        step "creating TLS client params..."+        cparams <- makeClientParams (CustomCAStore caPath)++        step "connecting via TLS..."+        conn <- TLS.connect+            defaultConnectInfo { ciUser = "testMySQLHaskell", ciDatabase = "testMySQLHaskell" }+            (cparams, "Winter")++        step "checking SSL cipher..."+        (_, is) <- query_ conn "SHOW STATUS LIKE 'Ssl_cipher'"+        Just row <- Stream.read is+        case row of+            [_name, MySQLText cipher] ->+                assertBool "SSL cipher is not empty" (not $ T.null cipher)+            _ -> assertFailure ("unexpected row shape: " ++ show row)+        Stream.skipToEof is++        close conn++    , testCaseSteps "TLS connection: prepared statement roundtrip" $ \step -> do+        step "creating TLS client params..."+        cparams <- makeClientParams (CustomCAStore caPath)++        step "connecting via TLS..."+        conn <- TLS.connect+            defaultConnectInfo { ciUser = "testMySQLHaskell", ciDatabase = "testMySQLHaskell" }+            (cparams, "Winter")++        step "executing prepared statement..."+        stmt <- prepareStmt conn "SELECT ? + 1"+        (_, is) <- queryStmt conn stmt [MySQLInt32 41]+        Just row <- Stream.read is+        assertBool "41 + 1 = 42"+            (row == [MySQLInt32 42] || row == [MySQLInt64 42])+        Stream.skipToEof is++        close conn+    ]
+ test/TextRow.hs view
@@ -0,0 +1,391 @@+{-# LANGUAGE NegativeLiterals    #-}+{-# LANGUAGE ScopedTypeVariables #-}++module TextRow where++import           Data.Time.Calendar  (fromGregorian)+import           Data.Time.LocalTime (LocalTime (..), TimeOfDay (..))+import           Database.MySQL.Base+import qualified System.IO.Streams   as Stream+import           Test.Tasty.HUnit+import qualified Data.Vector as V++tests :: MySQLConn -> Assertion+tests c = do+    (f, is1) <- query_ c "SELECT * FROM test"++    assertEqual "decode Field types" (columnType <$> f)+        [ mySQLTypeLong+        , mySQLTypeBit+        , mySQLTypeTiny+        , mySQLTypeTiny+        , mySQLTypeShort+        , mySQLTypeShort+        , mySQLTypeInt24+        , mySQLTypeInt24+        , mySQLTypeLong+        , mySQLTypeLong+        , mySQLTypeLongLong+        , mySQLTypeLongLong+        , mySQLTypeNewDecimal+        , mySQLTypeFloat+        , mySQLTypeDouble+        , mySQLTypeDate+        , mySQLTypeDateTime+        , mySQLTypeTimestamp+        , mySQLTypeTime+        , mySQLTypeYear+        , mySQLTypeString+        , mySQLTypeVarString+        , mySQLTypeString+        , mySQLTypeVarString+        , mySQLTypeBlob+        , mySQLTypeBlob+        , mySQLTypeBlob+        , mySQLTypeBlob+        , mySQLTypeString+        , mySQLTypeString+        ]++    Just v1 <- Stream.read is1+    assertEqual "decode NULL values" v1+        [ MySQLInt32 0+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        , MySQLNull+        ]++    Stream.skipToEof is1++    let bitV = 57514 -- 0b1110000010101010++    _ <- execute_ c "UPDATE test SET \+                \__bit        = b'1110000010101010'                    ,\+                \__tinyInt    = -128                                   ,\+                \__tinyIntU   = 255                                    ,\+                \__smallInt   = -32768                                 ,\+                \__smallIntU  = 65535                                  ,\+                \__mediumInt  = -8388608                               ,\+                \__mediumIntU = 16777215                               ,\+                \__int        = -2147483648                            ,\+                \__intU       = 4294967295                             ,\+                \__bigInt     = -9223372036854775808                   ,\+                \__bigIntU    = 18446744073709551615                   ,\+                \__decimal    = 1234567890.0123456789                  ,\+                \__float      = 3.14159                                ,\+                \__double     = 3.1415926535                           ,\+                \__date       = '2016-08-08'                           ,\+                \__datetime   = '2016-08-08 17:25:59'                  ,\+                \__timestamp  = '2016-08-08 17:25:59'                  ,\+                \__time       = '-199:59:59'                           ,\+                \__year       = 1999                                   ,\+                \__char       = '12345678'                             ,\+                \__varchar    = '韩冬真赞'                             ,\+                \__binary     = '12345678'                             ,\+                \__varbinary  = '12345678'                             ,\+                \__tinyblob   = '12345678'                             ,\+                \__tinytext   = '韩冬真赞'                             ,\+                \__blob       = '12345678'                             ,\+                \__text       = '韩冬真赞'                             ,\+                \__enum       = 'foo'                                  ,\+                \__set        = 'foo,bar' WHERE __id=0"++    (_, is2) <- query_ c "SELECT * FROM test"+    Just v2 <- Stream.read is2++    assertEqual "decode text protocol" v2+        [ MySQLInt32 0+        , MySQLBit bitV+        , MySQLInt8 (-128)+        , MySQLInt8U 255+        , MySQLInt16 (-32768)+        , MySQLInt16U 65535+        , MySQLInt32 (-8388608)+        , MySQLInt32U 16777215+        , MySQLInt32 (-2147483648)+        , MySQLInt32U 4294967295+        , MySQLInt64 (-9223372036854775808)+        , MySQLInt64U 18446744073709551615+        , MySQLDecimal 1234567890.0123456789+        , MySQLFloat 3.14159+        , MySQLDouble 3.1415926535+        , MySQLDate (fromGregorian 2016 08 08)+        , MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59))+        , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59))+        , MySQLTime 1 (TimeOfDay 199 59 59)+        , MySQLYear 1999+        , MySQLText "12345678"+        , MySQLText "韩冬真赞"+        , MySQLBytes "12345678"+        , MySQLBytes "12345678"+        , MySQLBytes "12345678"+        , MySQLText "韩冬真赞"+        , MySQLBytes "12345678"+        , MySQLText "韩冬真赞"+        , MySQLText "foo"+        , MySQLText "foo,bar"+        ]++    Stream.skipToEof is2++    (_, is3) <- queryVector_ c "SELECT * FROM test"+    Just v3 <- Stream.read is3+    Stream.skipToEof is3++    assertEqual "decode text protocol(queryVector_)" v2 (V.toList v3)++    _ <- execute c "UPDATE test SET \+            \__bit        = ?     ,\+            \__tinyInt    = ?     ,\+            \__tinyIntU   = ?     ,\+            \__smallInt   = ?     ,\+            \__smallIntU  = ?     ,\+            \__mediumInt  = ?     ,\+            \__mediumIntU = ?     ,\+            \__int        = ?     ,\+            \__intU       = ?     ,\+            \__bigInt     = ?     ,\+            \__bigIntU    = ?     ,\+            \__decimal    = ?     ,\+            \__float      = ?     ,\+            \__double     = ?     ,\+            \__date       = ?     ,\+            \__datetime   = ?     ,\+            \__timestamp  = ?     ,\+            \__time       = ?     ,\+            \__year       = ?     ,\+            \__char       = ?     ,\+            \__varchar    = ?     ,\+            \__binary     = ?     ,\+            \__varbinary  = ?     ,\+            \__tinyblob   = ?     ,\+            \__tinytext   = ?     ,\+            \__blob       = ?     ,\+            \__text       = ?     ,\+            \__enum       = ?     ,\+            \__set        = ? WHERE __id=0"+                [ MySQLBit bitV+                , MySQLInt8 (-128)+                , MySQLInt8U 255+                , MySQLInt16 (-32768)+                , MySQLInt16U 65535+                , MySQLInt32 (-8388608)+                , MySQLInt32U 16777215+                , MySQLInt32 (-2147483648)+                , MySQLInt32U 4294967295+                , MySQLInt64 (-9223372036854775808)+                , MySQLInt64U 18446744073709551615+                , MySQLDecimal 1234567890.0123456789+                , MySQLFloat 3.14159+                , MySQLDouble 3.1415926535+                , MySQLDate (fromGregorian 2016 08 08)+                , MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59))+                , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59))+                , MySQLTime 1 (TimeOfDay 199 59 59)+                , MySQLYear 1999+                , MySQLText "12345678"+                , MySQLText "韩冬真赞"+                , MySQLBytes "12345678"+                , MySQLBytes "12345678"+                , MySQLBytes "12345678"+                , MySQLText "韩冬真赞"+                , MySQLBytes "12345678"+                , MySQLText "韩冬真赞"+                , MySQLText "foo"+                , MySQLText "foo,bar"+                ]++    (_, is4) <- query_ c "SELECT * FROM test"+    Just v4 <- Stream.read is4++    assertEqual "roundtrip text protocol" v4+        [ MySQLInt32 0+        , MySQLBit bitV+        , MySQLInt8 (-128)+        , MySQLInt8U 255+        , MySQLInt16 (-32768)+        , MySQLInt16U 65535+        , MySQLInt32 (-8388608)+        , MySQLInt32U 16777215+        , MySQLInt32 (-2147483648)+        , MySQLInt32U 4294967295+        , MySQLInt64 (-9223372036854775808)+        , MySQLInt64U 18446744073709551615+        , MySQLDecimal 1234567890.0123456789+        , MySQLFloat 3.14159+        , MySQLDouble 3.1415926535+        , MySQLDate (fromGregorian 2016 08 08)+        , MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59))+        , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59))+        , MySQLTime 1 (TimeOfDay 199 59 59)+        , MySQLYear 1999+        , MySQLText "12345678"+        , MySQLText "韩冬真赞"+        , MySQLBytes "12345678"+        , MySQLBytes "12345678"+        , MySQLBytes "12345678"+        , MySQLText "韩冬真赞"+        , MySQLBytes "12345678"+        , MySQLText "韩冬真赞"+        , MySQLText "foo"+        , MySQLText "foo,bar"+        ]++    Stream.skipToEof is4+++    _ <- execute_ c "UPDATE test SET \+        \__mediumInt  = null         ,\+        \__double     = null         ,\+        \__text = null WHERE __id=0"++    (_, is5) <- query_ c "SELECT * FROM test"+    Just v5 <- Stream.read is5++    assertEqual "decode text protocol with null" v5+        [ MySQLInt32 0+        , MySQLBit bitV+        , MySQLInt8 (-128)+        , MySQLInt8U 255+        , MySQLInt16 (-32768)+        , MySQLInt16U 65535+        , MySQLNull+        , MySQLInt32U 16777215+        , MySQLInt32 (-2147483648)+        , MySQLInt32U 4294967295+        , MySQLInt64 (-9223372036854775808)+        , MySQLInt64U 18446744073709551615+        , MySQLDecimal 1234567890.0123456789+        , MySQLFloat 3.14159+        , MySQLNull+        , MySQLDate (fromGregorian 2016 08 08)+        , MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59))+        , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59))+        , MySQLTime 1 (TimeOfDay 199 59 59)+        , MySQLYear 1999+        , MySQLText "12345678"+        , MySQLText "韩冬真赞"+        , MySQLBytes "12345678"+        , MySQLBytes "12345678"+        , MySQLBytes "12345678"+        , MySQLText "韩冬真赞"+        , MySQLBytes "12345678"+        , MySQLNull+        , MySQLText "foo"+        , MySQLText "foo,bar"+        ]++    Stream.skipToEof is5++    _ <- execute c "UPDATE test SET \+        \__decimal  = ?         ,\+        \__date     = ?         ,\+        \__timestamp = ? WHERE __id=0"+        [MySQLNull, MySQLNull, MySQLNull]++    (_, is6) <- query_ c "SELECT * FROM test"+    Just v6 <- Stream.read is6++    assertEqual "roundtrip text protocol with null" v6+        [ MySQLInt32 0+        , MySQLBit bitV+        , MySQLInt8 (-128)+        , MySQLInt8U 255+        , MySQLInt16 (-32768)+        , MySQLInt16U 65535+        , MySQLNull+        , MySQLInt32U 16777215+        , MySQLInt32 (-2147483648)+        , MySQLInt32U 4294967295+        , MySQLInt64 (-9223372036854775808)+        , MySQLInt64U 18446744073709551615+        , MySQLNull+        , MySQLFloat 3.14159+        , MySQLNull+        , MySQLNull+        , MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59))+        , MySQLNull+        , MySQLTime 1 (TimeOfDay 199 59 59)+        , MySQLYear 1999+        , MySQLText "12345678"+        , MySQLText "韩冬真赞"+        , MySQLBytes "12345678"+        , MySQLBytes "12345678"+        , MySQLBytes "12345678"+        , MySQLText "韩冬真赞"+        , MySQLBytes "12345678"+        , MySQLNull+        , MySQLText "foo"+        , MySQLText "foo,bar"+        ]++    Stream.skipToEof is6++    _ <- execute_ c "UPDATE test SET \+        \__time       = '199:59:59'     ,\+        \__year       = 0  WHERE __id=0"++    (_, is7) <- query_ c "SELECT __time, __year FROM test"+    Just v7 <- Stream.read is7++    assertEqual "decode text protocol 2" v7+            [ MySQLTime 0 (TimeOfDay 199 59 59)+            , MySQLYear 0+            ]++    Stream.skipToEof is7++    _ <- execute_ c "UPDATE test SET \+        \__text       = ''     ,\+        \__blob       = ''  WHERE __id=0"++    (_, is8) <- query_ c "SELECT __text, __blob FROM test"+    Just v8 <- Stream.read is8++    assertEqual "decode text protocol 3" v8+            [ MySQLText ""+            , MySQLBytes ""+            ]++    Stream.skipToEof is8++    _ <- execute c "UPDATE test SET \+        \__time       = ?     ,\+        \__year       = ?  WHERE __id=0"+        [ MySQLTime 0 (TimeOfDay 199 59 59), MySQLYear 0]++    (_, is9) <- query_ c "SELECT __time, __year FROM test"+    Just v9 <- Stream.read is9++    assertEqual "roundtrip text protocol 2" v9+            [ MySQLTime 0 (TimeOfDay 199 59 59)+            , MySQLYear 0+            ]++    Stream.skipToEof is9
+ test/TextRowNew.hs view
@@ -0,0 +1,145 @@+{-# LANGUAGE NegativeLiterals    #-}+{-# LANGUAGE ScopedTypeVariables #-}++module TextRowNew where++import           Data.Time.Calendar  (fromGregorian)+import           Data.Time.LocalTime (LocalTime (..), TimeOfDay (..))+import           Database.MySQL.Base+import qualified System.IO.Streams   as Stream+import           Test.Tasty.HUnit++tests :: MySQLConn -> Assertion+tests c = do+    (f, is5) <- query_ c "SELECT * FROM test_new"++    assertEqual "decode Field types" (columnType <$> f)+        [ mySQLTypeLong+        , mySQLTypeDateTime+        , mySQLTypeTimestamp+        , mySQLTypeTime+        ]++    Just v1 <- Stream.read is5+    assertEqual "decode NULL values" v1+        [ MySQLInt32 0+        , MySQLNull+        , MySQLNull+        , MySQLNull+        ]++    Stream.skipToEof is5++    _ <- execute_ c "UPDATE test_new SET \+                \__datetime   = '2016-08-08 17:25:59.12'                  ,\+                \__timestamp  = '2016-08-08 17:25:59.1234'                ,\+                \__time       = '-199:59:59.123456' WHERE __id=0"++    (_, is4) <- query_ c "SELECT * FROM test_new"+    Just v2 <- Stream.read is4++    assertEqual "decode text protocol" v2+        [ MySQLInt32 0+        , MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.12))+        , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.1234))+        , MySQLTime 1 (TimeOfDay 199 59 59.123456)+        ]++    Stream.skipToEof is4++    _ <- execute_ c "UPDATE test_new SET \+                \__datetime   = '2016-08-08 17:25:59.1'                  ,\+                \__timestamp  = '2016-08-08 17:25:59.12'                ,\+                \__time       = '199:59:59.1234' WHERE __id=0"++    (_, is0) <- query_ c "SELECT * FROM test_new"+    Just v3 <- Stream.read is0++    assertEqual "decode text protocol 2" v3+        [ MySQLInt32 0+        , MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.1))+        , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.12))+        , MySQLTime 0 (TimeOfDay 199 59 59.1234)+        ]++    Stream.skipToEof is0++    _ <- execute c "UPDATE test_new SET \+            \__datetime   = ?     ,\+            \__timestamp  = ?     ,\+            \__time       = ?  WHERE __id=0"+                [ MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.12))+                , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.1234))+                , MySQLTime 1 (TimeOfDay 199 59 59.123456)+                ]+++    (_, is1) <- query_ c "SELECT * FROM test_new"+    Just v4 <- Stream.read is1++    assertEqual "roundtrip text protocol" v4+        [ MySQLInt32 0+        , MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.12))+        , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.1234))+        , MySQLTime 1 (TimeOfDay 199 59 59.123456)+        ]++    Stream.skipToEof is1++    _ <- execute c "UPDATE test_new SET \+            \__datetime   = ?     ,\+            \__timestamp  = ?     ,\+            \__time       = ?  WHERE __id=0"+                [ MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.1))+                , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.12))+                , MySQLTime 0 (TimeOfDay 199 59 59.1234)+                ]++    (_, is2) <- query_ c "SELECT * FROM test_new"+    Just v5 <- Stream.read is2++    assertEqual "roundtrip text protocol 2" v5+        [ MySQLInt32 0+        , MySQLDateTime (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.1))+        , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.12))+        , MySQLTime 0 (TimeOfDay 199 59 59.1234)+        ]++    Stream.skipToEof is2++    let row0 =+            [ MySQLInt32 0+            , MySQLDateTime  (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.10))+            , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 08) (TimeOfDay 17 25 59.12))+            , MySQLTime 0 (TimeOfDay 199 59 59.1234)+            ]+    let row1 =+            [ MySQLInt32 1+            , MySQLDateTime  (LocalTime (fromGregorian 2016 08 09) (TimeOfDay 18 25 59.10))+            , MySQLTimeStamp (LocalTime (fromGregorian 2016 08 09) (TimeOfDay 18 25 59.12))+            , MySQLTime 0 (TimeOfDay 299 59 59.1234)+            ]+    _ <- execute c "UPDATE test_new SET \+            \__id         = ?     ,\+            \__datetime   = ?     ,\+            \__timestamp  = ?     ,\+            \__time       = ?  WHERE __id=0"+            row0+    _ <- execute c "INSERT INTO test_new VALUES(\+            \?,\+            \?,\+            \?,\+            \? \+            \)"+            row1++    (_, is3) <- query c "SELECT * FROM test_new WHERE __id IN (?) ORDER BY __id" [Many [MySQLInt32 0, MySQLInt32 1]]+    Just v6 <- Stream.read is3+    Just v7 <- Stream.read is3++    assertEqual "select list of ids" [v6, v7] [row0, row1]++    Stream.skipToEof is3+    _ <- execute_ c "DELETE FROM test_new where __id=1"++    return ()
+ test/UnixSocket.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE ScopedTypeVariables #-}++module UnixSocket (tests, findSocketPath) where++import qualified Data.ByteString       as B+import           Database.MySQL.Base+import qualified System.IO.Streams     as Stream+import           System.Directory      (doesFileExist)+import           System.Environment    (lookupEnv)+import           Test.Tasty+import           Test.Tasty.HUnit++-- | Common Unix socket paths for MySQL/MariaDB.+defaultSocketPaths :: [FilePath]+defaultSocketPaths =+    [ "/tmp/mysql.sock"                    -- macOS Homebrew+    , "/var/run/mysqld/mysqld.sock"        -- Debian/Ubuntu+    , "/run/mysqld/mysqld.sock"            -- NixOS, Arch+    , "/var/lib/mysql/mysql.sock"          -- RHEL/CentOS+    , "/tmp/mysqld.sock"                   -- some macOS installs+    ]++-- | Find the MySQL Unix socket path.  Checks the @MYSQL_UNIX_SOCKET@+-- environment variable first, then probes common default locations.+findSocketPath :: IO (Maybe FilePath)+findSocketPath = do+    envPath <- lookupEnv "MYSQL_UNIX_SOCKET"+    case envPath of+        Just p  -> do+            exists <- doesFileExist p+            return $ if exists then Just p else Nothing+        Nothing -> firstExisting defaultSocketPaths+  where+    firstExisting []     = return Nothing+    firstExisting (p:ps) = do+        exists <- doesFileExist p+        if exists then return (Just p) else firstExisting ps++tests :: FilePath -> TestTree+tests socketPath = testGroup "unix-socket"+    [ testCaseSteps "connectUnixSocket: SELECT 1" $ \step -> do+        step "connecting via unix socket..."+        c <- connectUnixSocket socketPath defaultConnectInfo+            { ciUser     = "testMySQLHaskell"+            , ciDatabase = "testMySQLHaskell"+            }++        step "executing SELECT 1..."+        (_, is) <- query_ c "SELECT 1"+        Just row <- Stream.read is+        assertBool "SELECT 1 returns 1"+            (row == [MySQLInt32 1] || row == [MySQLInt64 1])+        Stream.skipToEof is++        close c++    , testCaseSteps "connectUnixSocketDetail: returns greeting" $ \step -> do+        step "connecting via unix socket with detail..."+        (greet, c) <- connectUnixSocketDetail socketPath defaultConnectInfo+            { ciUser     = "testMySQLHaskell"+            , ciDatabase = "testMySQLHaskell"+            }++        step "checking greeting version..."+        let ver = greetingVersion greet+        assertBool "greeting version is not empty" (not $ B.null ver)++        step "executing query to verify connection..."+        (_, is) <- query_ c "SELECT 1 + 1"+        Just row <- Stream.read is+        assertBool "SELECT 1+1 returns 2"+            (row == [MySQLInt32 2] || row == [MySQLInt64 2])+        Stream.skipToEof is++        close c++    , testCaseSteps "unix socket: prepared statement roundtrip" $ \step -> do+        step "connecting via unix socket..."+        c <- connectUnixSocket socketPath defaultConnectInfo+            { ciUser     = "testMySQLHaskell"+            , ciDatabase = "testMySQLHaskell"+            }++        step "executing prepared statement..."+        stmt <- prepareStmt c "SELECT ? + 1"+        (_, is) <- queryStmt c stmt [MySQLInt32 41]+        Just row <- Stream.read is+        assertBool "41 + 1 = 42"+            (row == [MySQLInt32 42] || row == [MySQLInt64 42])+        Stream.skipToEof is++        close c+    ]
+ test/Word24.hs view
@@ -0,0 +1,442 @@+module Word24(tests) where++import Prelude as P++import Test.Tasty+import           Test.Tasty.QuickCheck    (testProperty)++import Test.QuickCheck hiding ((.&.))++import Orphans()+import Data.Int+import Data.Int.Int24+import Data.Word+import Data.Word.Word24+import Data.Bits+import Foreign.Storable+import GHC.Real++prop_enum3 :: Int -> Bool+prop_addIdent :: Word24 -> Bool+prop_multIdent :: Word24 -> Bool+prop_unsigned :: Word24 -> Bool+prop_smaller :: Word16 -> Bool+prop_show :: Word16 -> Bool+prop_read :: Word24 -> Bool+prop_adder :: Word16 -> Word16 -> Property+prop_negate :: Word24 -> Bool+prop_abs :: Word24 -> Bool+prop_signum :: Word24 -> Bool+prop_real :: Word24 -> Bool+prop_enum1 :: Word24 -> Property+prop_enum2 :: Word24 -> Property+prop_enum4 :: Word24 -> Property+prop_enum5 :: Word24 -> Word8 -> Bool+prop_enum6 :: Word24 -> Word24 -> Bool+prop_quot :: Word24 -> Word24 -> Bool+prop_rem :: Word24 -> Word24 -> Bool+prop_div :: Word24 -> Word24 -> Bool+prop_mod :: Word24 -> Word24 -> Bool+prop_quotrem :: Word24 -> Word24 -> Bool+prop_divmod :: Word24 -> Word24 -> Bool+prop_and :: Word16 -> Word16 -> Bool+prop_or :: Word16 -> Word16 -> Bool+prop_xor :: Word16 -> Word16 -> Bool+prop_xor_ident :: Word24 -> Word24 -> Bool+prop_shiftL :: Word24 -> Bool+prop_shiftR :: Word24 -> Property+prop_shiftR2 :: Word24 -> Int -> Property+prop_shiftL_ident :: Word24 -> Bool+prop_rotate :: Word24 -> Int -> Bool+prop_comp :: Word24 -> Bool+prop_byteSwap :: Word24 -> Bool+prop_clz :: Word24 -> Bool+prop_ctz :: Word24 -> Bool+prop_bit_ident :: Bits a => a -> NonNegative Int -> Bool+prop_sizeOf :: Word24 -> Bool+prop_align :: Word24 -> Bool+prop_addIdentI :: Int24 -> Bool+prop_multIdentI :: Int24 -> Bool+prop_smallerI :: Int16 -> Bool+prop_showI :: Int16 -> Bool+prop_readI :: Int24 -> Bool+prop_adderI :: Int16 -> Int16 -> Bool+prop_negateI :: Int24 -> Bool+prop_absI :: Int24 -> Bool+prop_signumI :: Int24 -> Bool+prop_realI :: Int24 -> Bool+prop_enum1I :: Int24 -> Property+prop_enum2I :: Int24 -> Property+prop_enum3I :: Int -> Bool+prop_enum4I :: Int24 -> Property+prop_enum5I :: Int24 -> Word8 -> Bool+prop_enum6I :: Int24 -> Int24 -> Bool+prop_quotI :: Int24 -> Int24 -> Bool+prop_remI :: Int24 -> Int24 -> Bool+prop_divI :: Int24 -> Int24 -> Bool+prop_modI :: Int24 -> Int24 -> Bool++-- ----------------------------------------+-- Word24 Properties++prop_addIdent a = a - a == 0++prop_multIdent a = a * 1 == a++prop_unsigned a = a >= 0++prop_smaller a = (fromIntegral ((fromIntegral a) :: Word24) :: Word16) == a++prop_show a = show a == show (fromIntegral a :: Word24)++prop_read a = a == (read . show) a++prop_adder a b = (a < ub) && (b < ub) ==>+                 fromIntegral (a + b) ==+                 ((fromIntegral a + fromIntegral b) :: Word24)+     where+        ub :: Word16+        ub = maxBound `div` 2++prop_negate a = a == negate (negate a)++prop_abs a = a == abs a++prop_signum a = if a == 0 then signum a == a else signum a == 1++prop_real a = let r = toRational a in numerator r == fromIntegral a++-- Word24 Enum properties+prop_enum1 a = a < maxBound ==> succ a == a + 1++prop_enum2 a = a > minBound ==> pred a == a - 1++prop_enum3 a = let a' = abs a in+               toEnum a' == fromIntegral @_ @Integer a'++prop_enum4 a = a < maxBound ==> take 2 (enumFrom a) == [a, a+1]++prop_enum5 a b = let b' = fromIntegral b in+                 enumFromTo a (a + b') ==+                 map fromIntegral (enumFromTo (fromIntegral a :: Integer)+                                             (fromIntegral (a + b') :: Integer))++prop_enum6 a b = take 2 (enumFromThen a b) == [a,b]++-- Word24 Integral properties+prop_quot a b =+  quot a b == fromIntegral (quot (fromIntegral a :: Word32) (fromIntegral b :: Word32))++prop_rem a b =+  rem a b == fromIntegral (rem (fromIntegral a :: Word32) (fromIntegral b :: Word32))++prop_div a b =+  div a b == fromIntegral (div (fromIntegral a :: Word32) (fromIntegral b :: Word32))++prop_mod a b =+  mod a b == fromIntegral (mod (fromIntegral a :: Word32) (fromIntegral b :: Word32))++prop_quotrem a b = let (j, k) = quotRem a b in+  a == (b * j) + k++prop_divmod a b =+  divMod a b == (div a b, mod a b)++-- binary Word properties+prop_and a b = (a .&. b) == fromIntegral ( (fromIntegral a :: Word24) .&. (fromIntegral b :: Word24))++prop_or a b = (a .|. b) == fromIntegral ( (fromIntegral a :: Word24) .|. (fromIntegral b :: Word24))++prop_xor a b = (a `xor` b) == fromIntegral ( (fromIntegral a :: Word24) `xor` (fromIntegral b :: Word24))++prop_xor_ident a b = (a `xor` b) `xor` b == a++prop_shiftL a = a `shiftL` 1 == a * 2++prop_shiftR a = a < maxBound `div` 2 ==>+  (a * 2) `shift` (-1) == a++prop_shiftR2 a n = n >= 0 ==> a `shiftR` n == a `shift` (negate n)++prop_shiftL_ident a = a `shiftL` 0 == a++prop_rotate a b = (a `rotate` b) `rotate` (negate b) == a++prop_comp a = complement (complement a) == a++prop_byteSwap a = byteSwap24 (byteSwap24 a) == a++prop_clz a = countLeadingZeros a == countLeadingZeros' a+  where+    countLeadingZeros' :: Word24 -> Int+    countLeadingZeros' x = (w-1) - go (w-1)+      where+        go i | i < 0       = i -- no bit set+             | testBit x i = i+             | otherwise   = go (i-1)++        w = finiteBitSize x++prop_ctz a = countTrailingZeros a == countTrailingZeros' a+  where+    countTrailingZeros' :: Word24 -> Int+    countTrailingZeros' x = go 0+      where+        go i | i >= w      = i+             | testBit x i = i+             | otherwise   = go (i+1)+        w = finiteBitSize x++prop_bit_ident q (NonNegative j) = testBit (bit j `asTypeOf` q) j == (j < 24)++prop_popCount :: (Integral a1, Num a2, FiniteBits a1,+                        FiniteBits a2) =>+                       a1 -> a2 -> a1 -> Bool+prop_popCount s t a = if a >= 0+  then popCount (a `asTypeOf` s) == popCount (fromIntegral a `asTypeOf` t)+  else+    finiteBitSize s - popCount (a `asTypeOf` s) == finiteBitSize t - popCount (fromIntegral a `asTypeOf` t)++-- Word Storable properties+prop_sizeOf a = sizeOf a == 3++prop_align a = alignment a == 3++++-- ----------------------------------------+-- Int24 Properties++prop_addIdentI a = a - a == 0++prop_multIdentI a = a * 1 == a++prop_smallerI a = (fromIntegral ((fromIntegral a) :: Int24) :: Int16) == a++prop_showI a = show a == show (fromIntegral a :: Int24)++prop_readI a = a == (read . show) a++prop_adderI a b = ((fromIntegral a + fromIntegral b) :: Int) ==+                  fromIntegral ((fromIntegral a + fromIntegral b) :: Int24)++prop_negateI a = a == negate (negate a)++prop_absI a = if a >= 0 then a == abs a else a == negate (abs a)++prop_signumI a = signum a == fromIntegral (signum (fromIntegral a :: Int))++prop_realI a = let r = toRational a in numerator r == fromIntegral a++-- Int24 Enum Properties+prop_enum1I a = a < maxBound ==> succ a == a + 1++prop_enum2I a = a > minBound ==> pred a == a - 1++prop_enum3I a = let a' = abs a in+               toEnum a' == fromIntegral @_ @Integer a'++prop_enum4I a = a < maxBound ==> take 2 (enumFrom a) == [a, a+1]++prop_enum5I a b = let b' = fromIntegral b in+                 enumFromTo a (a + b') ==+                 map fromIntegral (enumFromTo (fromIntegral a :: Integer)+                                             (fromIntegral (a + b') :: Integer))++prop_enum6I a b = take 2 (enumFromThen a b) == [a,b]++-- Int24 Integral properties+prop_quotI a b =+  quot a b == fromIntegral (quot (fromIntegral a :: Int32) (fromIntegral b :: Int32))++prop_remI a b =+  rem a b == fromIntegral (rem (fromIntegral a :: Int32) (fromIntegral b :: Int32))++prop_divI a b =+  div a b == fromIntegral (div (fromIntegral a :: Int32) (fromIntegral b :: Int32))++prop_modI a b =+  mod a b == fromIntegral (mod (fromIntegral a :: Int32) (fromIntegral b :: Int32))++prop_quotremI :: Int24 -> Int24 -> Bool+prop_quotremI a b = let (j, k) = quotRem a b in+  a == (b * j) + k++prop_divmodI :: Int24 -> Int24 -> Bool+prop_divmodI a b =+  divMod a b == (div a b, mod a b)+++-- binary Int properties+prop_andI :: Int16 -> Int16 -> Bool+prop_andI a b = (a .&. b) == fromIntegral ( (fromIntegral a :: Int24) .&. (fromIntegral b :: Int24))++prop_orI :: Int16 -> Int16 -> Bool+prop_orI a b = (a .|. b) == fromIntegral ( (fromIntegral a :: Int24) .|. (fromIntegral b :: Int24))++prop_xorI :: Int16 -> Int16 -> Bool+prop_xorI a b = (a `xor` b) == fromIntegral ( (fromIntegral a :: Int24) `xor` (fromIntegral b :: Int24))++prop_xor_identI :: Int24 -> Int24 -> Bool+prop_xor_identI a b = (a `xor` b) `xor` b == a++prop_shiftLI :: Int24 -> Bool+prop_shiftLI a = a `shiftL` 1 == a * 2++prop_shiftL_identI :: Int24 -> Bool+prop_shiftL_identI a = a `shiftL` 0 == a++prop_shiftRI :: Int24 -> Property+prop_shiftRI a = (a < maxBound `div` 2) && (a > minBound `div` 2) ==>+  (a * 2) `shift` (-1) == a++prop_shiftR2I :: Int24 -> Int -> Property+prop_shiftR2I a n = n >= 0 ==> a `shiftR` n == a `shift` (negate n)++prop_rotateI :: Int24 -> Int -> Bool+prop_rotateI a b = (a `rotate` b) `rotate` (negate b) == a++prop_compI :: Int24 -> Bool+prop_compI a = complement (complement a) == a++prop_clzI :: Int24 -> Bool+prop_clzI a = countLeadingZeros a == countLeadingZeros' a+  where+    countLeadingZeros' :: Int24 -> Int+    countLeadingZeros' x = (w-1) - go (w-1)+      where+        go i | i < 0       = i -- no bit set+             | testBit x i = i+             | otherwise   = go (i-1)++        w = finiteBitSize x++prop_ctzI :: Int24 -> Bool+prop_ctzI a = countTrailingZeros a == countTrailingZeros' a+  where+    countTrailingZeros' :: Int24 -> Int+    countTrailingZeros' x = go 0+      where+        go i | i >= w      = i+             | testBit x i = i+             | otherwise   = go (i+1)+        w = finiteBitSize x++-- Int Storable properties+prop_sizeOfI :: Int24 -> Bool+prop_sizeOfI a = sizeOf a == 3++prop_alignI :: Int24 -> Bool+prop_alignI a = alignment a == 3+++-- ----------------------------------------+-- tests+tests :: [TestTree]+tests = [+ testGroup "Word24"+  [ testGroup "basic" [+    testProperty "add. identity" prop_addIdent+    ,testProperty "mult. identity" prop_multIdent+    ,testProperty "unsigned" prop_unsigned+    ,testProperty "Word16/Word24 conversion" prop_smaller+    ,testProperty "Show" prop_show+    ,testProperty "Read" prop_read+    ,testProperty "addition" prop_adder+    ,testProperty "negate identity" prop_negate+    ,testProperty "absolute value" prop_abs+    ,testProperty "signum" prop_signum+    ,testProperty "Real identity" prop_real+    ]+  ,testGroup "Integral instance" [+    testProperty  "quot" prop_quot+    ,testProperty "rem" prop_rem+    ,testProperty "div" prop_div+    ,testProperty "mod" prop_mod+    ,testProperty "quotRem" prop_quotrem+    ,testProperty "divmod" prop_divmod+    ]+  ,testGroup "Enum instance" [+    testProperty "enum succ" prop_enum1+    ,testProperty "enum pred" prop_enum2+    ,testProperty "toEnum" prop_enum3+    ,testProperty "enumFrom " prop_enum4+    ,testProperty "enumFromTo" prop_enum5+    ,testProperty "enumFromThen" prop_enum6+    ]+  ,testGroup "Bits instance" [+    testProperty "binary and" prop_and+    ,testProperty "binary or" prop_or+    ,testProperty "binary xor" prop_xor+    ,testProperty "binary xor identity" prop_xor_ident+    ,testProperty "binary shiftL" prop_shiftL+    ,testProperty "binary shiftL identity" prop_shiftL_ident+    ,testProperty "binary shift right" prop_shiftR+    ,testProperty "binary shiftR" prop_shiftR2+    ,testProperty "binary rotate" prop_rotate+    ,testProperty "binary complement" prop_comp+    ,testProperty "binary byteSwap24" prop_byteSwap+    ,testProperty "binary countLeadingZeros" prop_clz+    ,testProperty "binary countTrailingZeros" prop_ctz+    ,testProperty "binary countTrailingZeros 0" (prop_ctz (0::Word24))+    ,testProperty "bit/testBit" (prop_bit_ident (0::Word24))+    ,testProperty "popCount"    (prop_popCount (0::Word24) (0::Word))+    ]+  ,testGroup "Storable instance" [+    testProperty  "sizeOf Word24" prop_sizeOf+    ,testProperty "aligntment" prop_align+    ]+  ]+ ,testGroup "Int24"+  [testGroup "basic" [+    testProperty "add. identity" prop_addIdentI+    ,testProperty "mult. identity" prop_multIdentI+    ,testProperty "Int16/Int24 conversion" prop_smallerI+    ,testProperty "Show" prop_showI+    ,testProperty "Read" prop_readI+    ,testProperty "addition" prop_adderI+    ,testProperty "negate identity" prop_negateI+    ,testProperty "absolute value" prop_absI+    ,testProperty "signum" prop_signumI+    ,testProperty "Real identity" prop_realI+    ]+  ,testGroup "Integral instance" [+    testProperty  "quot" prop_quotI+    ,testProperty "rem" prop_remI+    ,testProperty "div" prop_divI+    ,testProperty "mod" prop_modI+    ,testProperty "quotRem" prop_quotremI+    ,testProperty "divmod" prop_divmodI+    ]+  ,testGroup "Enum instance" [+    testProperty "enum succ" prop_enum1I+    ,testProperty "enum pred" prop_enum2I+    ,testProperty "toEnum" prop_enum3I+    ,testProperty "enumFrom " prop_enum4I+    ,testProperty "enumFromTo" prop_enum5I+    ,testProperty "enumFromThen" prop_enum6I+    ]+  ,testGroup "Bits instance" [+    testProperty "binary and" prop_andI+    ,testProperty "binary or" prop_orI+    ,testProperty "binary xor" prop_xorI+    ,testProperty "binary xor identity" prop_xor_identI+    ,testProperty "binary shiftL" prop_shiftLI+    ,testProperty "binary shiftL identity" prop_shiftL_identI+    ,testProperty "binary shift right" prop_shiftRI+    ,testProperty "binary shiftR" prop_shiftR2I+    ,testProperty "binary rotate" prop_rotateI+    ,testProperty "binary complement" prop_compI+    ,testProperty "binary countLeadingZeros" prop_clzI+    ,testProperty "binary countTrailingZeros" prop_ctzI+    ,testProperty "binary countTrailingZeros 0" (prop_ctzI (0::Int24))+    ,testProperty "bit/testBit" (prop_bit_ident (0::Int24))+    ,testProperty "popCount"    (prop_popCount (0::Int24) (0::Int))+    ]+  ,testGroup "Storable instance" [+    testProperty  "sizeOf Int24" prop_sizeOfI+    ,testProperty "aligntment" prop_alignI+    ]+  ]+ ]+
+ test/cert/ca-key.pem view
@@ -0,0 +1,27 @@+-----BEGIN RSA PRIVATE KEY-----+MIIEpAIBAAKCAQEAro5f0Xvee3jDWdcbfAUriec9Hle2gwGH1gWhkVJvt8e6WfSi+LJEwEQJozgj3Mo8LNcd6J1/CwD7njNXE7IoGV9mqaC2b15EwzSkVFGz7M9EFH3rK+PsipFIlevgCNpIvjHPIaGXJ6g5iVm4MxnG1k5TjNrJFHV7MeYi/aJRaLQ1ZStMRS+U8e8b59XOzJ9jJBYE+p+KWWOzRMNkIUJEM1gOPSUrWChwD01LzMeyInpEkH305Or+DTF4KfUXqWD9xMk1/l3Qk0yMWi6nEjNb3oI6yk3VzXKv6UAuy5XlEYF7ldLKezCb+4sBXPzVXMu7TCHnQR7R4X/3pIQSIaezDvNil4QIDAQABAoIBAQCNIoasvQe1M35X+3InXa+K5HKOZLfhe0lT/IgxxkILNyIqPAzJA8J85kYYl2K/uTCQXNpM41L+FaxcT+EyCm1C6ux0p18zKHSFh5+VaW7krpqX4H5uMxglLbTPqyi4X63jQiikm5+4q9bhjA+5YGuZfDnOVdqyQgzC5tsbh5mUGUHaqREdecoWPkE5etId+zWOgq2yVTYgJaTYga1+sS9rtlHQTdgI5ZuEQe11X8JT40ghApo5glXXZnSPWeSA4hNJIKKTBllxrwBG0ZYI+BVf/pSiGciEz+fYrzv9JmPTjWEWutyr2AV5OMMnv1HTY5pkFFmylQTU8AHjFfUGD+Vlf7xmpFAoGBAOb0FZqBs6HWkU4FnWKe+48sO1bMEsAu3MO6Nk+o3eTd47J+bSXe+17UyGDSzGdnH1MxVeHmEqmfiXwkP9LEVr5ywbEqgzxFyRpXOXW+DxWs2boCJoGdW+FDdzjcJxigub/auSJasynowOjpYWLvWhlutVlhXC0XOhTejPUTh/xGKLAoGBAMF8+iuRLnIGIJ9anYo4RsKp26g/wW9JcwZS1HOeKH0sqmeFGwZDoDT2Dzq844tWSMfF0+eFoGWYZkvrtFogWGPjTLDWbagErDkSP6Y9qZkQFv+68nT8/G4k322ec6jx9H0/Z7+aWhlvGzPSpOnZc6XSu40MhMlCbCjPySr4To6hYLDAoGAZVReTiXt/Gm54vOPs/9w++8Y6WsC60Vs+PRnsTlMW2B2Kk3vow57cIYHTIPwbsXN9qBEwBJDyexXVAJLhlvie+zi6RtMSNVWhAE+YxFIZKDdoZ7Yd0uyIHSLUIU3GhUaqlR6udn1lcOrMAWFVvURIO+tz4b1XxejU8OccDUL+fO8DUCgYB+oM6ZiK6dy4kfH58NkaBIf9jBo7HEY/ZBJ6MK+GchPPktFNzR5bJU1vCypWpaB5dMPzgTyDhPLM+Xr1C+sY+YUUQutU7UAuSslG7r8+FuLiyPIz+ItwVR5InhSQEzAV7cFYH/6lV2fQ4n1RUw36/eMJMFnRU70awEwT6PVX+UXONVwKBgQCgVuQEukqt9NB8Hjte9w4MhAR22ML9XUvlDbQM2LL/eKj166SxXZqL+dPaV2pcBvET1NT4UMnSZTTS4ZqsTuuE6B9Dm9lWUqMPbBDtBKykOUWSTY88rRXSW+X0HdhQ/ycxHGN8Iv/wVZKo1ZSgEcEUM4jGvWclMgsnkWuaLBZPF/dA==+-----END RSA PRIVATE KEY-----
+ test/cert/ca.pem view
@@ -0,0 +1,21 @@+-----BEGIN CERTIFICATE-----+MIIDfzCCAmegAwIBAgIJAJkP4Ih73MLQMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV+BAYTAkNOMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX+aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMMBldpbnRlcjAeFw0xNzA3MjUxMjU3NDRa+Fw0yNzA2MDMxMjU3NDRaMFYxCzAJBgNVBAYTAkNOMRMwEQYDVQQIDApTb21lLVN0+YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMM+BldpbnRlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK6OX9F73nt4+w1nXG3wFK4nnPR5XtoMBh9YFoZFSb7fHuln0oiyRMBECaM4I9zKPCzXHeidfwsA++54zVxOyKBlfZqmgtm9eRMM0pFRRs+zPRBR96yj7IqRSJXr4AjaSL4xzyGhlyeoOY+lZuDMZxtZOU4zayRR1ezHmIv2iUWi0NWUrTEUlPHvG+fVzsyfYyQWBPqfilljs0T+DZCFCRDNYDj0lK1gocA9NS8zHsiJ6RJB99OTqw0xeCn1F6lg/cTJNf5d0JNMjFou+pxIzW96COspN1c1yr+lALsuV5RGBe5XSynswm+LAVz81VzLu0wh50Ee0eF/96SEE+iGnsw7zYpeECAwEAAaNQME4wHQYDVR0OBBYEFCj3eNkh8tyrzQMsEKirVA1ilVZA+MB8GA1UdIwQYMBaAFCj3eNkh8tyrzQMsEKirVA1ilVZAMAwGA1UdEwQFMAMBAf8w+DQYJKoZIhvcNAQELBQADggEBAFUf6Q6Og8PKYlGFLq177k+DIBJKMXqDUIWuZq/Z+o11vks/4uE9LRXkicPJUbjK7Iv6mw5gHwYSxAkrJaeY6wQpiV8PoSOD1AIm448da+4SLLJMnB4vPPqrnDhoHNNOWonsZYyV6avD97+kTxM6WgJYhdwM4L1mxGGGT37nuf+JtcZHLnI3PrHj3M89xwk4BFRXnb6+X/uWZxeF0AFCWZgMtzB+iMMz0VnBmBst8RI+Xs+gYzBXN1Mq1KMcSP5r+BGE1GAM46Q5EEI29M/5Fw/38MoZlMvg4K+rdqg7GAKF+fM3q9cMZ7Uv924KcQmF5ZKoZjFjp/PfaC8WZplF6yH+y3gA=+-----END CERTIFICATE-----
+ test/cert/server-cert.pem view
@@ -0,0 +1,20 @@+-----BEGIN CERTIFICATE-----+MIIDQTCCAimgAwIBAgIBATANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjET+MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ+dHkgTHRkMQ8wDQYDVQQDDAZXaW50ZXIwHhcNMTcwNzI1MTI1ODExWhcNMjcwNjAz+MTI1ODExWjBWMQswCQYDVQQGEwJDTjETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8G+A1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMQ8wDQYDVQQDDAZXaW50ZXIw+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCD3Wlrhbz++Z360JuvsXD+M/CMquiGJU9XSA35g5lw7nIEsNYKzOW3dGuTzTLo/ZCw006H7eLVIZn0AZVSBSnk+teEvOHUdn56E3XVvCGBxBdbTbVrT+2hDClVg230102SYLqNwwH1fOCGVnq74sHSA+86FRLlt+uFuZCpflaXsZ6lz/dNWAP4Lwu2bA05YJAn3mxEkrlxxbsLL4X+MKpUMg+WRed+eykaFTY8G+pXmJZPLYblv5Vy+Jm5sbBuIqUElFkfHtHTpdCvahe2hcOmDcG+eKmoSav2kolFb1xov7tOXCC9DdhfuNPLE0KGAWBLu8nMDmk7apOI33U6+4OCS8Xn+AgMBAAGjGjAYMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgXgMA0GCSqGSIb3DQEBCwUA+A4IBAQBOKHB1StdKjQzk9cDFroR8jvbTb0PTANQYFsyAtN/VXcRLK9GxkW8Hxn/o+SO+gTStOUu4ZaS6EymJ/z0z1oHDTCifoPo3R9o3SCUH7XDM3PV3ulgG59c2tfzRw+dpK+ttfCe1e228YKfP3WI1hNnMjVnijKCJMROumQiY4CQhtUDdaAdK5jF2yvtThb+kyHuimeIoyBESBmPuGnkFtcwzxXW43Qt9Y2uJg7WxyEgoNfXL73Enx++Hv6UJSPM+sUPItS8M5wFoNJw4hpjPvXxQLrVnWd2Qs8p3LmohBpogg9C7GTzTrsM3wySxRTEA+rrbjCtYnbrs4+HD9mlbzKi9mrcXi+-----END CERTIFICATE-----
+ test/cert/server-key.pem view
@@ -0,0 +1,27 @@+-----BEGIN RSA PRIVATE KEY-----+MIIEogIBAAKCAQEAwg91pa4W8/vmd+tCbr7FwzPwjKrohiVPV0gN+YOZcO5yBLDW+Cszlt3Rrk80y6P2QsNNOh+3i1SGZ9AGVUgUp5LXhLzh1HZ+ehN11bwhgcQXW021a+0/toQwpVYNt9NdNkmC6jcMB9XzghlZ6u+LB0gPOhUS5bfrhbmQqX5Wl7Gepc/3TV+gD+C8LtmwNOWCQJ95sRJK5ccW7Cy+F/jCqVDIFkXnfnspGhU2PBvqV5iWTy2G5b++VcviZubGwbiKlBJRZHx7R06XQr2oXtoXDpg3BnipqEmr9pKJRW9caL+7TlwgvQ3Y+X7jTyxNChgFgS7vJzA5pO2qTiN91OvuDgkvF5wIDAQABAoIBAFG0gpscPivBrlpl+IhZQmiFC2A/R0Y7RXKNqAgBA6TTEIBtbfPMVK87e5IBZUAsK34WjohfXD78eo5w5+om3jIK1b6pCgG6c6Ulqsh0oVY82SYM2p6CkCW/aF/kaGgyRLebodQNEzKlYR4woE+wrSQTq0QZqYwd3NWGbLI5AVCMQNKjk/ogHOGAxwNGAzB3KHG55zQL68iu77vSJFa+gf9MtSgaqxosWedtdRWVo9N2jm3BhrebK85J5bZQUp6xHvHSgbW/PGWQMiJUzuzo+wzmBhkE4RM5hgBtBBUs6lP/trxzl1eKEkDSQdsxpZ5bIz0dhe3ZUqHM/apTVoD1n+OBYTfWkCgYEA8Ue08d2C44wJTqyY1a+Tf0kSrNFEYjqWTKe19nzlQ7mFoUP6ORIw+z6pLvliFSFFxQrB2uAzgJGIYdUY62XCV7Cm7gIfQxJEv2jUJ8ZNFTyZGKvBVxQu6+98CDkmsK5Zgjd/fhZb/7c6a9tdNm6C0Z/tcvi/CqhCiK5UmBYTXOeuUCgYEAzeZH+buv65edEsxesHxZjSFIekEnvUUQw8JpRn/Cr9nzOem9quBwp85PNAoEPkBiDeq6C+2xE6wFO0C17cw5oPahkTg0C+PbKmNA3HWZLYXjAEke+bXponEGiBKtrcuoXPiRVi+dLXVQyZ09sSd9QFVz0zb+yhnxmB8ckz6m3Qn1NsCgYBiMBo0qz5Ot3g6KgbwPsk5+bVVKOscnakwr2bw1GNJ74w28eNtlSj/O4rB8P63Npkb/KqzFbYfxhnIJl5DvV9/L+AZiqT5rnw2XD2P2474OvGBZJ9xAQZhuqIw/oD+OZV3Znpdp1+9CqWdquGd+w8Fm5+2Xwh6Mibi5wqfuVF2A20+QKBgCXlQjR6Hj1+KTjFHJrEJhXDd3ibAv70DAXcgTVs+uFmvctk59GxaUqZQB6V0V4bv3BMSV2c2bpfDUBnfpcYibr/aSasYmWsFnG1sM8qa+qlP7hnvNm8z3lkUA1vUqPwHdausSEoLkjJHFXDWmzmOvkTmMLdi2AROT4pOu81j1+MfeNAoGAIbS1Gngub9JDeoyWPRPNkZaNXIaPdkXOXmwJvigbnWstgbHsgu24HfJc+LCkS8MIYWVSbceDZflkSaJ9ZF0dGEJPPNdsfPi3x2iWTYHp8as9+8JnSsG4C8fvA+gQZLrF0JxP7B4ZmHYNawtQBVnrcKldwCuotP16V7lH+we3xuezQ=+-----END RSA PRIVATE KEY-----
+ test/cert/server-req.pem view
@@ -0,0 +1,16 @@+-----BEGIN CERTIFICATE REQUEST-----+MIICmzCCAYMCAQAwVjELMAkGA1UEBhMCQ04xEzARBgNVBAgMClNvbWUtU3RhdGUx+ITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEPMA0GA1UEAwwGV2lu+dGVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwg91pa4W8/vmd+tC+br7FwzPwjKrohiVPV0gN+YOZcO5yBLDWCszlt3Rrk80y6P2QsNNOh+3i1SGZ9AGV+UgUp5LXhLzh1HZ+ehN11bwhgcQXW021a0/toQwpVYNt9NdNkmC6jcMB9XzghlZ6u++LB0gPOhUS5bfrhbmQqX5Wl7Gepc/3TVgD+C8LtmwNOWCQJ95sRJK5ccW7Cy+F/j+CqVDIFkXnfnspGhU2PBvqV5iWTy2G5b+VcviZubGwbiKlBJRZHx7R06XQr2oXtoX+Dpg3BnipqEmr9pKJRW9caL+7TlwgvQ3YX7jTyxNChgFgS7vJzA5pO2qTiN91OvuD+gkvF5wIDAQABoAAwDQYJKoZIhvcNAQELBQADggEBAIjdOfU/dtQcP/Da1IIXlLji+wFCHk5/jlC+oxSIXKtF+3ZBrCx3R/pm7V9ERbeJWXswlhd+8Rkeyde+L3PnDly77+F2oKa1jKcp3lituP1V7tVM8WvScZbLC3RAKFioapN9+Jbwhs8DVtKryNiAtndjFl+jRG3+7xS5rOijgVTStzqvYfRqcUJvvgdT62EV2oSq59ZjNmYkvj8ZAUuI8feKDNm+TdTdGnaC0Br83XNiTKtsSLKk9p0WudzZkiCaeR07hOA1AoJ30DSnsQE+BG/5f/nn+1RbEybGDaFPTULYGXsoZ4TTcr5sxgfXmhomBn/iF/Ns5LEvNosKqwOOPPM4P0eI=+-----END CERTIFICATE REQUEST-----
+ test/json-data/buffer-builder.json view
@@ -0,0 +1,4322 @@+[+  {+    "_id": "54727a90e43524efe8abfdac",+    "index": 0,+    "guid": "aaa926e8-8359-4afe-adb0-dca0401de765",+    "isActive": true,+    "balance": "$1,114.73",+    "picture": "http://placehold.it/32x32",+    "age": 20,+    "eyeColor": "green",+    "name": "Dillon Valenzuela",+    "gender": "male",+    "company": "GEOFORM",+    "email": "dillonvalenzuela@geoform.com",+    "phone": "+1 (817) 536-3796",+    "address": "309 Desmond Court, Zarephath, Montana, 6156",+    "about": "Dolore laboris excepteur sunt velit velit nostrud non esse qui laboris nostrud Lorem. Amet anim occaecat et ipsum. Ad proident nostrud elit adipisicing anim labore consectetur ut amet elit. Tempor aliqua in ea eiusmod elit Lorem cillum est laboris. Nisi Lorem amet amet Lorem. Nostrud sunt incididunt magna nostrud amet enim. Laboris ad et fugiat dolor ad proident aliquip nisi do ea eiusmod enim proident.\r\n",+    "registered": "2014-08-17T07:29:03 +07:00",+    "latitude": -17.423462,+    "longitude": 121.069662,+    "tags": [+      "sunt",+      "ex",+      "irure",+      "cillum",+      "velit",+      "duis",+      "sit"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Joyce Jordan"+      },+      {+        "id": 1,+        "name": "Ophelia Rosales"+      },+      {+        "id": 2,+        "name": "Florine Stark"+      }+    ],+    "greeting": "Hello, Dillon Valenzuela! You have 9 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a90f520a07ff2498a69",+    "index": 1,+    "guid": "8f78cc18-a7ef-498b-9adf-60f008a48c02",+    "isActive": false,+    "balance": "$1,324.52",+    "picture": "http://placehold.it/32x32",+    "age": 33,+    "eyeColor": "blue",+    "name": "Donovan Weiss",+    "gender": "male",+    "company": "TELPOD",+    "email": "donovanweiss@telpod.com",+    "phone": "+1 (962) 417-2408",+    "address": "511 Grant Avenue, Westmoreland, Hawaii, 5092",+    "about": "Laborum excepteur exercitation sint veniam dolor. Dolor aliqua esse do aliqua adipisicing. Ea magna minim veniam tempor ipsum. Incididunt anim reprehenderit occaecat proident eiusmod anim Lorem aliquip dolore incididunt. Consequat occaecat qui minim commodo est velit ex.\r\n",+    "registered": "2014-09-30T23:49:41 +07:00",+    "latitude": -18.034963,+    "longitude": 70.821705,+    "tags": [+      "cupidatat",+      "excepteur",+      "id",+      "consectetur",+      "ea",+      "enim",+      "et"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Cummings Irwin"+      },+      {+        "id": 1,+        "name": "Conley Lester"+      },+      {+        "id": 2,+        "name": "Katy Holt"+      }+    ],+    "greeting": "Hello, Donovan Weiss! You have 2 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a90fbb3f49ea6d40dde",+    "index": 2,+    "guid": "69a4c6d7-6189-4a90-ae00-7b64ab1d5a2d",+    "isActive": false,+    "balance": "$2,739.87",+    "picture": "http://placehold.it/32x32",+    "age": 33,+    "eyeColor": "brown",+    "name": "Renee Calderon",+    "gender": "female",+    "company": "TALKOLA",+    "email": "reneecalderon@talkola.com",+    "phone": "+1 (910) 523-3382",+    "address": "323 Montauk Avenue, Greenwich, South Dakota, 8034",+    "about": "Reprehenderit deserunt et do proident Lorem Lorem dolor labore ullamco eu nulla laborum voluptate. Consectetur sint magna enim commodo dolore ullamco in. Nulla id proident esse veniam proident esse velit exercitation do. Qui nulla magna anim Lorem dolor culpa id. Nostrud cupidatat officia culpa sit irure voluptate exercitation voluptate nostrud elit. Duis occaecat velit fugiat exercitation anim aute laborum irure.\r\n",+    "registered": "2014-09-11T20:48:20 +07:00",+    "latitude": 15.654081,+    "longitude": 84.156609,+    "tags": [+      "dolor",+      "et",+      "in",+      "veniam",+      "non",+      "est",+      "enim"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Gilda Murray"+      },+      {+        "id": 1,+        "name": "Marguerite West"+      },+      {+        "id": 2,+        "name": "Chrystal Rojas"+      }+    ],+    "greeting": "Hello, Renee Calderon! You have 1 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a90c5ea2a48502a0640",+    "index": 3,+    "guid": "689a64d7-16b0-41dc-947d-035b40a29ed4",+    "isActive": false,+    "balance": "$3,805.39",+    "picture": "http://placehold.it/32x32",+    "age": 32,+    "eyeColor": "blue",+    "name": "Aguirre Garrett",+    "gender": "male",+    "company": "ZOSIS",+    "email": "aguirregarrett@zosis.com",+    "phone": "+1 (811) 471-3145",+    "address": "198 Baycliff Terrace, Clarence, District Of Columbia, 1528",+    "about": "Deserunt minim aliquip magna ut fugiat quis. Quis consequat culpa reprehenderit minim do eiusmod adipisicing nostrud aute. Lorem nostrud laborum quis dolore tempor tempor magna aute eu pariatur dolor est. Reprehenderit ex consectetur adipisicing Lorem nostrud fugiat aute non quis anim laborum aute proident amet. Sunt consectetur eu ut eu mollit ipsum.\r\n",+    "registered": "2014-02-14T04:27:27 +08:00",+    "latitude": 25.922024,+    "longitude": 107.428199,+    "tags": [+      "esse",+      "officia",+      "commodo",+      "esse",+      "cupidatat",+      "amet",+      "ad"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Penelope Humphrey"+      },+      {+        "id": 1,+        "name": "Church Byers"+      },+      {+        "id": 2,+        "name": "Hazel Stafford"+      }+    ],+    "greeting": "Hello, Aguirre Garrett! You have 4 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a9096bcf2d2f4b4aeff",+    "index": 4,+    "guid": "77392422-81ad-48c8-9cfc-37d142a814d5",+    "isActive": false,+    "balance": "$1,956.06",+    "picture": "http://placehold.it/32x32",+    "age": 26,+    "eyeColor": "brown",+    "name": "Griffin Mclaughlin",+    "gender": "male",+    "company": "COMTOUR",+    "email": "griffinmclaughlin@comtour.com",+    "phone": "+1 (950) 494-2956",+    "address": "549 Robert Street, Libertytown, North Carolina, 4377",+    "about": "Exercitation culpa ad id deserunt sit ullamco anim anim ad aute officia ut tempor. Ullamco dolor pariatur enim exercitation commodo consectetur fugiat velit nulla mollit pariatur occaecat. Amet voluptate qui labore mollit ipsum consequat. Esse pariatur proident ad pariatur aliquip labore quis voluptate quis ut culpa tempor. Reprehenderit sint id officia deserunt quis laboris. Velit ipsum dolore anim consectetur.\r\n",+    "registered": "2014-06-23T04:48:12 +07:00",+    "latitude": 8.808284,+    "longitude": 20.539814,+    "tags": [+      "consequat",+      "proident",+      "eiusmod",+      "dolore",+      "in",+      "laborum",+      "non"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Hinton Santiago"+      },+      {+        "id": 1,+        "name": "Talley Dyer"+      },+      {+        "id": 2,+        "name": "Karen Logan"+      }+    ],+    "greeting": "Hello, Griffin Mclaughlin! You have 3 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a9010e5c5ac55919561",+    "index": 5,+    "guid": "7e6fca4d-0329-47ab-b3e7-f4d6a6ee4dd2",+    "isActive": true,+    "balance": "$3,767.46",+    "picture": "http://placehold.it/32x32",+    "age": 36,+    "eyeColor": "brown",+    "name": "Rachel Mcclure",+    "gender": "female",+    "company": "ESCENTA",+    "email": "rachelmcclure@escenta.com",+    "phone": "+1 (857) 518-3454",+    "address": "901 Harwood Place, Rossmore, Tennessee, 3213",+    "about": "Nisi enim deserunt ut ex mollit fugiat. Magna nostrud culpa irure nulla mollit nisi. Sit ea incididunt amet culpa veniam voluptate Lorem sunt dolore amet. Exercitation tempor eiusmod exercitation officia dolor. Ut ea sint veniam cupidatat et. Sunt nostrud ad ipsum non elit elit esse ut dolor. Et officia et minim ipsum amet sint eu ut nostrud adipisicing cillum ullamco.\r\n",+    "registered": "2014-05-29T20:42:11 +07:00",+    "latitude": 8.132878,+    "longitude": -99.198755,+    "tags": [+      "est",+      "Lorem",+      "ullamco",+      "nisi",+      "reprehenderit",+      "nisi",+      "cupidatat"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Michael Martin"+      },+      {+        "id": 1,+        "name": "Gayle Heath"+      },+      {+        "id": 2,+        "name": "Slater Bender"+      }+    ],+    "greeting": "Hello, Rachel Mcclure! You have 2 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a90a76a5ba627b180c9",+    "index": 6,+    "guid": "fbf59485-142c-4eb6-a98e-8952cbb19942",+    "isActive": true,+    "balance": "$2,139.12",+    "picture": "http://placehold.it/32x32",+    "age": 28,+    "eyeColor": "blue",+    "name": "Megan Morrow",+    "gender": "female",+    "company": "PETICULAR",+    "email": "meganmorrow@peticular.com",+    "phone": "+1 (825) 572-2854",+    "address": "151 Jardine Place, Boonville, Idaho, 160",+    "about": "Magna reprehenderit aliqua quis eiusmod eu minim ut. Consectetur non veniam est et aliqua mollit. Aliqua eiusmod sit fugiat qui quis nisi mollit officia do. Enim quis quis occaecat ullamco anim reprehenderit exercitation. Ad id id laborum ipsum ipsum cillum ipsum occaecat irure. Commodo voluptate ullamco excepteur ea laboris adipisicing veniam cillum commodo velit anim. Occaecat magna laborum non ex anim consequat dolor cillum excepteur velit duis commodo officia veniam.\r\n",+    "registered": "2014-04-16T08:56:52 +07:00",+    "latitude": 57.771446,+    "longitude": -146.885322,+    "tags": [+      "veniam",+      "officia",+      "magna",+      "ipsum",+      "nulla",+      "ea",+      "mollit"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Marina Wise"+      },+      {+        "id": 1,+        "name": "Ann Mercado"+      },+      {+        "id": 2,+        "name": "Glenn Duran"+      }+    ],+    "greeting": "Hello, Megan Morrow! You have 7 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a90776f7328f4df750f",+    "index": 7,+    "guid": "ef99cc91-061c-49c5-9901-3ac22a75bc92",+    "isActive": true,+    "balance": "$3,161.69",+    "picture": "http://placehold.it/32x32",+    "age": 37,+    "eyeColor": "blue",+    "name": "Lawson Kirby",+    "gender": "male",+    "company": "PLASMOSIS",+    "email": "lawsonkirby@plasmosis.com",+    "phone": "+1 (853) 583-2577",+    "address": "122 Linden Boulevard, Edneyville, Palau, 6050",+    "about": "Elit ullamco reprehenderit eiusmod in duis minim exercitation incididunt reprehenderit eiusmod nulla. Cupidatat ea anim incididunt esse ea eu esse laboris velit. Culpa sunt exercitation officia amet voluptate enim duis ipsum ex anim quis nisi. Dolor officia irure do veniam do amet ullamco eu.\r\n",+    "registered": "2014-08-13T02:14:13 +07:00",+    "latitude": -16.166232,+    "longitude": 12.254111,+    "tags": [+      "et",+      "aliqua",+      "aute",+      "voluptate",+      "officia",+      "est",+      "quis"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Marquita Hutchinson"+      },+      {+        "id": 1,+        "name": "Karla Conner"+      },+      {+        "id": 2,+        "name": "Mattie Page"+      }+    ],+    "greeting": "Hello, Lawson Kirby! You have 3 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a90b8e9fff0ed297a59",+    "index": 8,+    "guid": "9bb8d813-704c-4714-929f-1321576a2701",+    "isActive": false,+    "balance": "$1,275.37",+    "picture": "http://placehold.it/32x32",+    "age": 23,+    "eyeColor": "brown",+    "name": "Pat Medina",+    "gender": "female",+    "company": "PLEXIA",+    "email": "patmedina@plexia.com",+    "phone": "+1 (933) 557-3028",+    "address": "623 Sunnyside Court, Edinburg, Minnesota, 2620",+    "about": "Est aliqua sint adipisicing labore magna sunt laboris non eu. Aute deserunt commodo Lorem amet sunt amet qui qui incididunt anim Lorem culpa. Ea exercitation officia dolor aute incididunt ea enim. Nulla aliqua ut ullamco sunt velit cillum esse minim incididunt ut mollit aute. Fugiat eiusmod anim laborum duis aliquip aute reprehenderit qui dolor officia amet labore id. Enim officia reprehenderit incididunt sit. Nisi ullamco nisi adipisicing esse eiusmod dolore veniam cupidatat consequat sunt.\r\n",+    "registered": "2014-10-22T20:29:14 +07:00",+    "latitude": -9.032987,+    "longitude": 71.98971,+    "tags": [+      "laborum",+      "dolore",+      "aliqua",+      "tempor",+      "eu",+      "elit",+      "voluptate"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Elnora Moses"+      },+      {+        "id": 1,+        "name": "Alexandra Carter"+      },+      {+        "id": 2,+        "name": "Beth Pacheco"+      }+    ],+    "greeting": "Hello, Pat Medina! You have 5 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a908c17a619571cfd39",+    "index": 9,+    "guid": "0a5de094-eadc-4120-8941-964f27fbc898",+    "isActive": true,+    "balance": "$1,939.34",+    "picture": "http://placehold.it/32x32",+    "age": 35,+    "eyeColor": "brown",+    "name": "Gibbs Butler",+    "gender": "male",+    "company": "OPTIQUE",+    "email": "gibbsbutler@optique.com",+    "phone": "+1 (956) 574-3434",+    "address": "552 Columbia Street, Richmond, Illinois, 903",+    "about": "Laboris eu ut magna exercitation non laboris sint. Mollit velit est exercitation enim exercitation reprehenderit culpa eiusmod laboris fugiat veniam adipisicing. Voluptate enim id do eu officia duis in incididunt. Esse do laboris pariatur cupidatat elit.\r\n",+    "registered": "2014-02-24T15:46:23 +08:00",+    "latitude": -68.874573,+    "longitude": -6.876165,+    "tags": [+      "excepteur",+      "deserunt",+      "ea",+      "labore",+      "aliqua",+      "deserunt",+      "labore"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Chen Langley"+      },+      {+        "id": 1,+        "name": "Latonya Vaughan"+      },+      {+        "id": 2,+        "name": "Simone Sutton"+      }+    ],+    "greeting": "Hello, Gibbs Butler! You have 2 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a9049447027a378812c",+    "index": 10,+    "guid": "406ae437-820d-4e0f-b8dc-1da0ddcc561c",+    "isActive": false,+    "balance": "$1,348.37",+    "picture": "http://placehold.it/32x32",+    "age": 39,+    "eyeColor": "brown",+    "name": "Bond Peck",+    "gender": "male",+    "company": "DANCERITY",+    "email": "bondpeck@dancerity.com",+    "phone": "+1 (918) 554-2856",+    "address": "361 Cortelyou Road, Hollins, Missouri, 131",+    "about": "Nisi ut ea ut deserunt. Consequat pariatur dolor consequat sunt. Ullamco aute proident excepteur fugiat id consequat eu nisi occaecat.\r\n",+    "registered": "2014-05-15T02:12:14 +07:00",+    "latitude": -79.266187,+    "longitude": 92.89454,+    "tags": [+      "ex",+      "est",+      "sit",+      "irure",+      "qui",+      "veniam",+      "exercitation"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Nettie Pope"+      },+      {+        "id": 1,+        "name": "Alyssa Hensley"+      },+      {+        "id": 2,+        "name": "Puckett Kinney"+      }+    ],+    "greeting": "Hello, Bond Peck! You have 7 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a9062a82eff9506c4d7",+    "index": 11,+    "guid": "b7400b79-2b71-46cd-9260-13e0767b437e",+    "isActive": true,+    "balance": "$1,757.66",+    "picture": "http://placehold.it/32x32",+    "age": 20,+    "eyeColor": "blue",+    "name": "Garrison Warner",+    "gender": "male",+    "company": "NAMEBOX",+    "email": "garrisonwarner@namebox.com",+    "phone": "+1 (874) 417-2183",+    "address": "927 Knapp Street, Shaft, Connecticut, 2188",+    "about": "Sint commodo ipsum commodo id magna labore in. Pariatur nostrud reprehenderit adipisicing amet enim ut quis laboris culpa exercitation deserunt cillum. Adipisicing excepteur sunt do labore anim exercitation non excepteur. Minim consectetur sit dolore occaecat et aliquip aliquip excepteur aliquip ut do eiusmod ea nulla. Nostrud sunt dolore ea sunt est enim occaecat culpa minim. Sit elit mollit mollit aliquip eu ex. Tempor nulla voluptate dolore ex.\r\n",+    "registered": "2014-06-18T03:58:19 +07:00",+    "latitude": -87.629414,+    "longitude": 96.653639,+    "tags": [+      "eu",+      "deserunt",+      "dolore",+      "nisi",+      "culpa",+      "ullamco",+      "ipsum"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Gutierrez Sears"+      },+      {+        "id": 1,+        "name": "Holly Jacobson"+      },+      {+        "id": 2,+        "name": "Henry Delgado"+      }+    ],+    "greeting": "Hello, Garrison Warner! You have 4 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a904638f48ca7f9e85e",+    "index": 12,+    "guid": "4cd59615-46b7-4733-a439-dc59c0ca0118",+    "isActive": true,+    "balance": "$3,897.06",+    "picture": "http://placehold.it/32x32",+    "age": 24,+    "eyeColor": "blue",+    "name": "Consuelo Bray",+    "gender": "female",+    "company": "COMTEST",+    "email": "consuelobray@comtest.com",+    "phone": "+1 (945) 432-2620",+    "address": "433 Clinton Street, Clarktown, Maine, 6916",+    "about": "Do occaecat consectetur tempor Lorem ullamco commodo exercitation veniam reprehenderit nulla officia. Nisi irure sunt cupidatat amet et excepteur quis et do. Et commodo nulla occaecat do sint quis mollit reprehenderit do laboris magna. Dolore ea sit aute esse consequat fugiat Lorem occaecat adipisicing adipisicing qui aute elit. Amet veniam anim ipsum aute officia. Fugiat nisi magna anim pariatur minim magna exercitation duis officia.\r\n",+    "registered": "2014-03-19T07:39:44 +07:00",+    "latitude": 7.760448,+    "longitude": -92.399314,+    "tags": [+      "irure",+      "veniam",+      "ex",+      "ullamco",+      "anim",+      "do",+      "enim"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Ellen Bullock"+      },+      {+        "id": 1,+        "name": "Davis Vaughn"+      },+      {+        "id": 2,+        "name": "Robert Mendoza"+      }+    ],+    "greeting": "Hello, Consuelo Bray! You have 8 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a90fd85a45d33d54cf7",+    "index": 13,+    "guid": "53868eb7-df5c-445d-ad62-6124db081b6c",+    "isActive": false,+    "balance": "$2,988.04",+    "picture": "http://placehold.it/32x32",+    "age": 21,+    "eyeColor": "green",+    "name": "Clark Skinner",+    "gender": "male",+    "company": "OVOLO",+    "email": "clarkskinner@ovolo.com",+    "phone": "+1 (834) 587-2391",+    "address": "967 Dewey Place, Gibsonia, Puerto Rico, 6398",+    "about": "Duis irure exercitation ullamco amet aliquip minim sit consequat officia laborum id ad. Lorem duis velit consequat non irure nulla elit elit velit dolor ullamco qui fugiat. Quis ad velit ea consectetur veniam Lorem incididunt quis magna eiusmod. Et nostrud irure tempor nostrud esse.\r\n",+    "registered": "2014-10-11T06:48:59 +07:00",+    "latitude": -62.633073,+    "longitude": -121.03338,+    "tags": [+      "ut",+      "deserunt",+      "adipisicing",+      "enim",+      "amet",+      "proident",+      "officia"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Francine Petersen"+      },+      {+        "id": 1,+        "name": "Lena Robles"+      },+      {+        "id": 2,+        "name": "Mcintyre Gardner"+      }+    ],+    "greeting": "Hello, Clark Skinner! You have 6 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a908789850c55818269",+    "index": 14,+    "guid": "84cc7d0b-0544-40f8-900a-825429b0ee87",+    "isActive": true,+    "balance": "$3,012.07",+    "picture": "http://placehold.it/32x32",+    "age": 32,+    "eyeColor": "blue",+    "name": "Katie Allen",+    "gender": "female",+    "company": "SYBIXTEX",+    "email": "katieallen@sybixtex.com",+    "phone": "+1 (959) 504-2563",+    "address": "543 Heath Place, Bartonsville, New Mexico, 4882",+    "about": "Nisi ullamco anim in culpa in non nostrud ex aliqua ex. Nisi occaecat exercitation sint ad magna Lorem amet anim pariatur quis. Dolore consequat quis dolor ea. Aliquip sunt ex cillum tempor fugiat laboris ipsum mollit aliqua reprehenderit nulla fugiat exercitation ad.\r\n",+    "registered": "2014-03-17T05:45:50 +07:00",+    "latitude": -79.18988,+    "longitude": -95.023826,+    "tags": [+      "commodo",+      "incididunt",+      "voluptate",+      "sit",+      "mollit",+      "veniam",+      "et"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Hampton Jimenez"+      },+      {+        "id": 1,+        "name": "Lara Cline"+      },+      {+        "id": 2,+        "name": "Jennings Cantu"+      }+    ],+    "greeting": "Hello, Katie Allen! You have 3 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a90787015f61850162f",+    "index": 15,+    "guid": "d848ac32-1d24-471a-ad7e-d272f52cb5fc",+    "isActive": true,+    "balance": "$1,416.33",+    "picture": "http://placehold.it/32x32",+    "age": 38,+    "eyeColor": "brown",+    "name": "Pratt Watson",+    "gender": "male",+    "company": "HARMONEY",+    "email": "prattwatson@harmoney.com",+    "phone": "+1 (905) 557-3197",+    "address": "867 Campus Road, Lindisfarne, Northern Mariana Islands, 8349",+    "about": "Nisi mollit eu reprehenderit qui ut eu cillum cupidatat do. Et ex qui minim proident. Quis aliquip aliquip exercitation exercitation dolor aute nostrud fugiat ea. Id ullamco occaecat reprehenderit mollit occaecat ea ipsum cupidatat ea consequat fugiat.\r\n",+    "registered": "2014-11-15T05:07:03 +08:00",+    "latitude": -66.117872,+    "longitude": 52.197654,+    "tags": [+      "ullamco",+      "sunt",+      "ea",+      "veniam",+      "elit",+      "sint",+      "id"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Dale Ray"+      },+      {+        "id": 1,+        "name": "Beryl Hayes"+      },+      {+        "id": 2,+        "name": "Millie Good"+      }+    ],+    "greeting": "Hello, Pratt Watson! You have 2 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a90c4106283f0c1eed9",+    "index": 16,+    "guid": "6839b0af-5c69-45e2-b83b-381ff05034e6",+    "isActive": false,+    "balance": "$2,720.67",+    "picture": "http://placehold.it/32x32",+    "age": 31,+    "eyeColor": "blue",+    "name": "Shari Mendez",+    "gender": "female",+    "company": "INDEXIA",+    "email": "sharimendez@indexia.com",+    "phone": "+1 (895) 589-3754",+    "address": "533 Conduit Boulevard, Forestburg, Mississippi, 6873",+    "about": "Ex excepteur elit id laborum veniam dolore duis ea eiusmod minim. Consequat reprehenderit magna elit tempor voluptate esse officia minim reprehenderit consequat fugiat qui eiusmod. Et Lorem nisi voluptate tempor nulla cillum reprehenderit pariatur elit dolore occaecat laboris sunt. Anim cupidatat do veniam pariatur nostrud anim eu aliqua veniam ad cillum quis.\r\n",+    "registered": "2014-05-04T10:15:37 +07:00",+    "latitude": 31.931465,+    "longitude": 155.732674,+    "tags": [+      "enim",+      "sint",+      "est",+      "aliqua",+      "elit",+      "laboris",+      "occaecat"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Desiree Norris"+      },+      {+        "id": 1,+        "name": "Abigail Crosby"+      },+      {+        "id": 2,+        "name": "Anderson Alston"+      }+    ],+    "greeting": "Hello, Shari Mendez! You have 6 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a90474ecbf0a9c34382",+    "index": 17,+    "guid": "079bb238-0556-4ede-8243-2eaadabb3050",+    "isActive": false,+    "balance": "$2,629.01",+    "picture": "http://placehold.it/32x32",+    "age": 26,+    "eyeColor": "green",+    "name": "Hancock Rowland",+    "gender": "male",+    "company": "SOFTMICRO",+    "email": "hancockrowland@softmicro.com",+    "phone": "+1 (983) 565-2302",+    "address": "168 Dooley Street, Driftwood, North Dakota, 3607",+    "about": "Eu voluptate velit incididunt ipsum amet veniam. Lorem aliquip adipisicing incididunt laborum aliqua elit consectetur aute velit voluptate ea. Mollit irure duis consequat veniam sint nostrud est dolore sunt anim laboris fugiat. Dolor officia elit esse elit dolore. Minim non est duis culpa ullamco veniam excepteur. Veniam ullamco amet ullamco esse voluptate laboris velit.\r\n",+    "registered": "2014-09-06T04:32:40 +07:00",+    "latitude": -68.853851,+    "longitude": 135.592762,+    "tags": [+      "nulla",+      "ut",+      "magna",+      "adipisicing",+      "officia",+      "nulla",+      "cillum"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Shannon Burch"+      },+      {+        "id": 1,+        "name": "Marisol Nixon"+      },+      {+        "id": 2,+        "name": "Poole Wolfe"+      }+    ],+    "greeting": "Hello, Hancock Rowland! You have 4 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a90cf708eccc9032fd0",+    "index": 18,+    "guid": "8930775e-9271-400e-b662-77ee520b3369",+    "isActive": false,+    "balance": "$1,026.68",+    "picture": "http://placehold.it/32x32",+    "age": 33,+    "eyeColor": "blue",+    "name": "Luna Rice",+    "gender": "male",+    "company": "GEEKULAR",+    "email": "lunarice@geekular.com",+    "phone": "+1 (840) 541-2996",+    "address": "417 Varick Avenue, Martell, Vermont, 2749",+    "about": "Aliqua enim ullamco do eiusmod. Ipsum incididunt minim esse veniam nisi velit. Ipsum reprehenderit velit eiusmod nisi magna dolore consectetur reprehenderit magna culpa. Commodo excepteur labore proident adipisicing in velit sit occaecat reprehenderit id.\r\n",+    "registered": "2014-10-15T05:13:55 +07:00",+    "latitude": -57.335675,+    "longitude": -173.387514,+    "tags": [+      "fugiat",+      "enim",+      "exercitation",+      "aliquip",+      "Lorem",+      "laboris",+      "deserunt"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Mclaughlin Schmidt"+      },+      {+        "id": 1,+        "name": "Munoz Key"+      },+      {+        "id": 2,+        "name": "Candace Stephenson"+      }+    ],+    "greeting": "Hello, Luna Rice! You have 2 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a90a0fd17c9e4f5d33f",+    "index": 19,+    "guid": "570f0e20-085f-4c40-9e84-b555cc4e79c8",+    "isActive": true,+    "balance": "$3,333.73",+    "picture": "http://placehold.it/32x32",+    "age": 39,+    "eyeColor": "green",+    "name": "Beverly Newton",+    "gender": "female",+    "company": "ECLIPSENT",+    "email": "beverlynewton@eclipsent.com",+    "phone": "+1 (902) 595-3072",+    "address": "903 Knight Court, Gardiner, Rhode Island, 3327",+    "about": "Labore culpa tempor nisi commodo dolore exercitation est eu. Aute exercitation duis proident deserunt culpa ullamco. Irure cillum elit ea sint dolore ipsum adipisicing anim sit. Amet incididunt labore ex fugiat mollit quis reprehenderit nisi ut excepteur proident ullamco laboris. Reprehenderit voluptate aliqua labore culpa in non magna velit nostrud amet. Minim nulla ea culpa irure. Minim ea ex nisi proident ex ex quis ipsum culpa.\r\n",+    "registered": "2014-09-23T10:25:17 +07:00",+    "latitude": 23.973865,+    "longitude": -71.604537,+    "tags": [+      "ullamco",+      "anim",+      "id",+      "pariatur",+      "est",+      "cupidatat",+      "qui"+    ],+    "friends": [+      {+        "id": 0,+        "name": "James Garner"+      },+      {+        "id": 1,+        "name": "Goff Reese"+      },+      {+        "id": 2,+        "name": "Casey Bradford"+      }+    ],+    "greeting": "Hello, Beverly Newton! You have 2 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a90fd0cafc2fa513aea",+    "index": 20,+    "guid": "c0e243a3-bf20-4112-8dab-0b0aed785060",+    "isActive": true,+    "balance": "$2,290.97",+    "picture": "http://placehold.it/32x32",+    "age": 28,+    "eyeColor": "blue",+    "name": "Stokes Dodson",+    "gender": "male",+    "company": "SLAMBDA",+    "email": "stokesdodson@slambda.com",+    "phone": "+1 (994) 443-2829",+    "address": "232 Kosciusko Street, Manitou, Michigan, 2638",+    "about": "Nostrud minim cillum elit reprehenderit ut do eu. Quis deserunt enim nisi do proident quis quis voluptate elit. Sit nostrud et amet nostrud pariatur exercitation cillum ullamco laborum ullamco do id. Tempor incididunt dolore consequat ut amet in reprehenderit magna anim laboris reprehenderit tempor adipisicing.\r\n",+    "registered": "2014-05-21T16:30:03 +07:00",+    "latitude": -86.112825,+    "longitude": 56.089279,+    "tags": [+      "ea",+      "dolor",+      "dolor",+      "consequat",+      "nulla",+      "cillum",+      "voluptate"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Jodi Camacho"+      },+      {+        "id": 1,+        "name": "Michele Miles"+      },+      {+        "id": 2,+        "name": "Rene Mitchell"+      }+    ],+    "greeting": "Hello, Stokes Dodson! You have 9 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a908be67242a33d9597",+    "index": 21,+    "guid": "8ad92a47-ac1d-46ad-9dc3-d1f96526bf1d",+    "isActive": true,+    "balance": "$1,134.78",+    "picture": "http://placehold.it/32x32",+    "age": 20,+    "eyeColor": "green",+    "name": "Jody Stewart",+    "gender": "female",+    "company": "EARGO",+    "email": "jodystewart@eargo.com",+    "phone": "+1 (925) 470-3426",+    "address": "271 Norwood Avenue, Sperryville, Oklahoma, 9504",+    "about": "Ut cillum ea pariatur Lorem ad veniam ex ullamco. Exercitation culpa sit occaecat aliquip eu quis magna exercitation quis reprehenderit nostrud consequat. Exercitation magna duis culpa nulla id proident. Adipisicing sunt consequat laborum consequat id dolore aute qui. Consectetur magna irure minim ut consectetur elit qui elit voluptate sint excepteur officia.\r\n",+    "registered": "2014-04-29T09:04:08 +07:00",+    "latitude": -51.736959,+    "longitude": -37.406481,+    "tags": [+      "officia",+      "ullamco",+      "velit",+      "nulla",+      "ipsum",+      "eiusmod",+      "mollit"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Jodie Browning"+      },+      {+        "id": 1,+        "name": "Collins Gamble"+      },+      {+        "id": 2,+        "name": "Emerson Mccullough"+      }+    ],+    "greeting": "Hello, Jody Stewart! You have 4 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a905956037e359d6786",+    "index": 22,+    "guid": "730467a3-9f5d-4d41-9503-09f30222dffa",+    "isActive": false,+    "balance": "$1,821.23",+    "picture": "http://placehold.it/32x32",+    "age": 24,+    "eyeColor": "brown",+    "name": "Wall Moss",+    "gender": "male",+    "company": "BOILICON",+    "email": "wallmoss@boilicon.com",+    "phone": "+1 (865) 518-2689",+    "address": "902 Fleet Street, Cornfields, West Virginia, 9088",+    "about": "Consectetur voluptate enim est elit dolor adipisicing reprehenderit ut in proident sunt. Fugiat cillum anim ullamco qui laborum sit. Dolore qui mollit ut irure proident consequat est veniam. Qui incididunt quis duis enim quis cupidatat do nostrud dolore labore consectetur laboris consectetur. Adipisicing veniam amet officia mollit incididunt non veniam ea laboris. Enim adipisicing velit in dolore proident Lorem esse et sunt.\r\n",+    "registered": "2014-01-21T18:36:59 +08:00",+    "latitude": -70.265889,+    "longitude": -148.213176,+    "tags": [+      "ea",+      "cillum",+      "ea",+      "enim",+      "aliqua",+      "nostrud",+      "do"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Margaret Chavez"+      },+      {+        "id": 1,+        "name": "Mendez Garrison"+      },+      {+        "id": 2,+        "name": "Howard Ortiz"+      }+    ],+    "greeting": "Hello, Wall Moss! You have 9 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a9062feb6c7bfa8710d",+    "index": 23,+    "guid": "1b428a55-1b50-4bcf-b2d9-06c2e8950768",+    "isActive": true,+    "balance": "$1,045.36",+    "picture": "http://placehold.it/32x32",+    "age": 25,+    "eyeColor": "blue",+    "name": "Adele Santana",+    "gender": "female",+    "company": "EBIDCO",+    "email": "adelesantana@ebidco.com",+    "phone": "+1 (981) 528-2138",+    "address": "117 Kent Avenue, Chamizal, Iowa, 4289",+    "about": "Culpa eu nostrud mollit voluptate consequat pariatur. Qui ex laboris velit cupidatat enim. Reprehenderit commodo eu excepteur ex elit amet dolore enim. Nisi dolor pariatur enim laborum voluptate Lorem magna ipsum enim aute do sunt. Ad consectetur velit laborum magna labore Lorem occaecat quis. Exercitation ipsum velit nostrud cupidatat consequat ex non velit deserunt pariatur officia. Magna occaecat Lorem qui aliqua id aliquip laborum.\r\n",+    "registered": "2014-10-01T21:15:54 +07:00",+    "latitude": 24.339451,+    "longitude": -38.920718,+    "tags": [+      "ullamco",+      "labore",+      "magna",+      "fugiat",+      "consectetur",+      "nostrud",+      "veniam"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Sharpe Haley"+      },+      {+        "id": 1,+        "name": "Patrice Horne"+      },+      {+        "id": 2,+        "name": "Melanie Sosa"+      }+    ],+    "greeting": "Hello, Adele Santana! You have 10 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a900e0eec7c0c194713",+    "index": 24,+    "guid": "cc7114a5-89ce-4fb6-9ad8-b5886360ab42",+    "isActive": true,+    "balance": "$2,505.31",+    "picture": "http://placehold.it/32x32",+    "age": 36,+    "eyeColor": "blue",+    "name": "Lindsey Colon",+    "gender": "male",+    "company": "XYLAR",+    "email": "lindseycolon@xylar.com",+    "phone": "+1 (952) 532-3896",+    "address": "144 Poly Place, Detroit, Federated States Of Micronesia, 2004",+    "about": "Ullamco nostrud Lorem duis duis aute sunt commodo Lorem. Esse cupidatat aliqua et laborum officia et. Ea magna est eiusmod id sit ea esse consectetur qui.\r\n",+    "registered": "2014-04-05T02:52:07 +07:00",+    "latitude": -55.350071,+    "longitude": -20.446672,+    "tags": [+      "id",+      "sit",+      "cillum",+      "id",+      "exercitation",+      "reprehenderit",+      "deserunt"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Kathleen Berger"+      },+      {+        "id": 1,+        "name": "Morales Tran"+      },+      {+        "id": 2,+        "name": "Dionne Hunter"+      }+    ],+    "greeting": "Hello, Lindsey Colon! You have 3 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a90a20ec5bce57f2f55",+    "index": 25,+    "guid": "c1d3f95b-25dd-4b48-8fe8-9af5f62895e2",+    "isActive": true,+    "balance": "$1,609.28",+    "picture": "http://placehold.it/32x32",+    "age": 21,+    "eyeColor": "green",+    "name": "Langley Moore",+    "gender": "male",+    "company": "ZYTRAX",+    "email": "langleymoore@zytrax.com",+    "phone": "+1 (912) 580-3688",+    "address": "747 Abbey Court, Odessa, Kentucky, 4688",+    "about": "Eiusmod adipisicing aliquip quis voluptate qui. Enim esse culpa elit id eu dolor culpa eiusmod. Deserunt ut ea ipsum ipsum non culpa ullamco non ipsum sunt cupidatat.\r\n",+    "registered": "2014-06-28T08:29:15 +07:00",+    "latitude": 65.464748,+    "longitude": -129.638837,+    "tags": [+      "deserunt",+      "cillum",+      "sint",+      "proident",+      "ad",+      "adipisicing",+      "mollit"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Mcguire Vance"+      },+      {+        "id": 1,+        "name": "Norma Buck"+      },+      {+        "id": 2,+        "name": "Janelle Bailey"+      }+    ],+    "greeting": "Hello, Langley Moore! You have 8 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a903c9a63ef7fccce96",+    "index": 26,+    "guid": "88c32c0f-a2c6-49c2-8261-bc323895067f",+    "isActive": false,+    "balance": "$3,748.41",+    "picture": "http://placehold.it/32x32",+    "age": 26,+    "eyeColor": "blue",+    "name": "Duran Bryan",+    "gender": "male",+    "company": "BULLZONE",+    "email": "duranbryan@bullzone.com",+    "phone": "+1 (911) 598-2827",+    "address": "903 Village Road, Cressey, New Jersey, 9174",+    "about": "Do in fugiat est deserunt in sit Lorem quis reprehenderit est ut tempor. Ullamco in veniam non quis cillum consectetur id. Elit exercitation reprehenderit et amet voluptate magna voluptate minim veniam enim exercitation aute aute esse. Exercitation magna voluptate laborum ipsum cupidatat exercitation ad eu aute deserunt et. Voluptate qui aute ut elit amet excepteur nostrud officia irure do.\r\n",+    "registered": "2014-01-29T12:32:05 +08:00",+    "latitude": -51.269785,+    "longitude": 79.015891,+    "tags": [+      "consequat",+      "id",+      "exercitation",+      "incididunt",+      "adipisicing",+      "laboris",+      "laboris"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Jenny Allison"+      },+      {+        "id": 1,+        "name": "Della Hobbs"+      },+      {+        "id": 2,+        "name": "Bradshaw Owens"+      }+    ],+    "greeting": "Hello, Duran Bryan! You have 5 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a907a2f07fbb3efec0f",+    "index": 27,+    "guid": "df997a84-5ec1-4f63-acb7-140b9fed981d",+    "isActive": true,+    "balance": "$1,249.61",+    "picture": "http://placehold.it/32x32",+    "age": 28,+    "eyeColor": "blue",+    "name": "Juarez House",+    "gender": "male",+    "company": "ZUVY",+    "email": "juarezhouse@zuvy.com",+    "phone": "+1 (841) 533-3140",+    "address": "494 Atlantic Avenue, Shasta, Nevada, 8475",+    "about": "Proident eu nostrud laboris adipisicing pariatur sint ipsum exercitation velit non incididunt. Qui cupidatat reprehenderit nostrud magna sunt aute nulla aliquip velit culpa commodo fugiat laborum. Dolore consectetur esse labore magna anim exercitation dolor velit qui sunt Lorem reprehenderit amet.\r\n",+    "registered": "2014-03-16T19:21:13 +07:00",+    "latitude": 57.96486,+    "longitude": 130.460435,+    "tags": [+      "incididunt",+      "reprehenderit",+      "consequat",+      "duis",+      "veniam",+      "nulla",+      "dolor"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Keri Lara"+      },+      {+        "id": 1,+        "name": "Celina Pace"+      },+      {+        "id": 2,+        "name": "Bush Haynes"+      }+    ],+    "greeting": "Hello, Juarez House! You have 7 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a903021f42c063be598",+    "index": 28,+    "guid": "1dc6349d-4354-42a9-9f73-7bc12d5ac9c2",+    "isActive": false,+    "balance": "$1,991.33",+    "picture": "http://placehold.it/32x32",+    "age": 26,+    "eyeColor": "blue",+    "name": "Sonja Horton",+    "gender": "female",+    "company": "VIDTO",+    "email": "sonjahorton@vidto.com",+    "phone": "+1 (990) 504-2432",+    "address": "612 River Street, Carrizo, California, 5778",+    "about": "Nisi reprehenderit occaecat anim ut minim aliqua aliquip velit veniam eiusmod reprehenderit laboris ad. Duis aliqua labore cillum proident est amet. Adipisicing nostrud proident labore laborum. Elit est sint ex veniam reprehenderit. Veniam ut dolor nulla tempor voluptate sit excepteur sit ut quis.\r\n",+    "registered": "2014-10-30T05:07:28 +07:00",+    "latitude": 85.37031,+    "longitude": -122.082636,+    "tags": [+      "tempor",+      "ex",+      "nostrud",+      "ullamco",+      "ullamco",+      "nisi",+      "cillum"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Phillips Henry"+      },+      {+        "id": 1,+        "name": "Leanne Sexton"+      },+      {+        "id": 2,+        "name": "Alisha Chang"+      }+    ],+    "greeting": "Hello, Sonja Horton! You have 1 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a903a33ff6bb03e049a",+    "index": 29,+    "guid": "4ef3a65d-5407-400b-aace-62bbb8a83c6a",+    "isActive": true,+    "balance": "$2,823.03",+    "picture": "http://placehold.it/32x32",+    "age": 21,+    "eyeColor": "blue",+    "name": "Lidia Hewitt",+    "gender": "female",+    "company": "PROWASTE",+    "email": "lidiahewitt@prowaste.com",+    "phone": "+1 (909) 562-3312",+    "address": "625 Ralph Avenue, Kiskimere, Maryland, 2033",+    "about": "Eu adipisicing eiusmod laboris veniam sunt id ad eiusmod consectetur nisi. Esse nulla ipsum Lorem nisi consequat laborum. Veniam ea exercitation ullamco pariatur consectetur. Cupidatat velit excepteur ex eu sunt id. In velit reprehenderit pariatur velit. Voluptate nostrud deserunt qui mollit et tempor nostrud eiusmod adipisicing ad est laborum. Fugiat nisi sit nulla eu ad.\r\n",+    "registered": "2014-01-15T00:05:42 +08:00",+    "latitude": -50.566931,+    "longitude": -149.036259,+    "tags": [+      "in",+      "ipsum",+      "tempor",+      "magna",+      "aliqua",+      "duis",+      "incididunt"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Allie Baker"+      },+      {+        "id": 1,+        "name": "Valdez Giles"+      },+      {+        "id": 2,+        "name": "Knox Vargas"+      }+    ],+    "greeting": "Hello, Lidia Hewitt! You have 3 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a90ce7ef56e53f98790",+    "index": 30,+    "guid": "9ae34b38-098b-4de6-aa0f-90359571ddf2",+    "isActive": false,+    "balance": "$3,152.82",+    "picture": "http://placehold.it/32x32",+    "age": 38,+    "eyeColor": "brown",+    "name": "Nieves Ewing",+    "gender": "male",+    "company": "QNEKT",+    "email": "nievesewing@qnekt.com",+    "phone": "+1 (888) 460-2384",+    "address": "425 Highland Place, Hampstead, Washington, 7019",+    "about": "Ipsum aliquip ullamco labore aliquip et. Eu pariatur exercitation ex fugiat fugiat. Nulla voluptate et cupidatat deserunt mollit et incididunt. Deserunt anim qui cupidatat veniam et et dolore qui sunt tempor ipsum. In et voluptate excepteur incididunt dolore cupidatat exercitation ullamco dolore Lorem nostrud sunt nostrud. Occaecat amet ut laborum laborum aliqua est incididunt ea excepteur commodo. Ipsum et elit nisi duis pariatur occaecat.\r\n",+    "registered": "2014-06-15T18:44:29 +07:00",+    "latitude": 8.010315,+    "longitude": 69.049129,+    "tags": [+      "exercitation",+      "eiusmod",+      "voluptate",+      "ullamco",+      "consectetur",+      "reprehenderit",+      "enim"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Maggie Rodriguez"+      },+      {+        "id": 1,+        "name": "Evans Roach"+      },+      {+        "id": 2,+        "name": "Greer Mccoy"+      }+    ],+    "greeting": "Hello, Nieves Ewing! You have 7 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a9049aa4bbedefd311d",+    "index": 31,+    "guid": "471d52a6-da38-4015-8d7a-6fe215a032d3",+    "isActive": false,+    "balance": "$1,456.93",+    "picture": "http://placehold.it/32x32",+    "age": 21,+    "eyeColor": "green",+    "name": "Reyna Clay",+    "gender": "female",+    "company": "KINETICA",+    "email": "reynaclay@kinetica.com",+    "phone": "+1 (865) 425-3747",+    "address": "480 Dakota Place, Kimmell, Utah, 6677",+    "about": "Tempor esse ex amet aliqua incididunt incididunt qui commodo laborum incididunt deserunt elit id consequat. Proident duis ut ullamco non ea magna fugiat fugiat Lorem minim. Incididunt tempor nostrud proident irure laboris consectetur elit labore adipisicing fugiat nulla quis ipsum consequat. Tempor mollit pariatur irure in nisi minim magna excepteur. Aliquip et laborum cillum adipisicing consequat.\r\n",+    "registered": "2014-10-30T08:53:04 +07:00",+    "latitude": 38.437675,+    "longitude": 42.847793,+    "tags": [+      "incididunt",+      "est",+      "mollit",+      "nulla",+      "ipsum",+      "ea",+      "ea"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Glenna Macdonald"+      },+      {+        "id": 1,+        "name": "Bertha Meadows"+      },+      {+        "id": 2,+        "name": "Oneill Barnes"+      }+    ],+    "greeting": "Hello, Reyna Clay! You have 6 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a900238319b9b407aa0",+    "index": 32,+    "guid": "fa5c52e1-9a9d-44d4-8e6b-5a65221ab142",+    "isActive": true,+    "balance": "$3,113.15",+    "picture": "http://placehold.it/32x32",+    "age": 24,+    "eyeColor": "blue",+    "name": "Felicia Harris",+    "gender": "female",+    "company": "FITCORE",+    "email": "feliciaharris@fitcore.com",+    "phone": "+1 (814) 449-2730",+    "address": "641 Hazel Court, Bawcomville, Wisconsin, 4111",+    "about": "Magna in elit aute ipsum anim dolore et exercitation sunt occaecat velit. Sint tempor nulla sit laborum dolor ex irure nisi nulla veniam in labore laborum. Nulla magna excepteur exercitation aute occaecat consequat laborum Lorem velit occaecat aliqua qui. Id in aliqua dolore fugiat duis elit laborum commodo cillum ex aute minim eu.\r\n",+    "registered": "2014-04-15T21:31:39 +07:00",+    "latitude": -58.891583,+    "longitude": -162.554134,+    "tags": [+      "eiusmod",+      "dolor",+      "enim",+      "cupidatat",+      "labore",+      "sunt",+      "consectetur"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Rich Lott"+      },+      {+        "id": 1,+        "name": "Rebecca Cardenas"+      },+      {+        "id": 2,+        "name": "Jarvis Barron"+      }+    ],+    "greeting": "Hello, Felicia Harris! You have 7 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a90f03941c94fe50d08",+    "index": 33,+    "guid": "49070cf3-b2f3-445e-86ec-3ae9cddc474d",+    "isActive": false,+    "balance": "$1,105.05",+    "picture": "http://placehold.it/32x32",+    "age": 35,+    "eyeColor": "green",+    "name": "Josefina Holland",+    "gender": "female",+    "company": "GOGOL",+    "email": "josefinaholland@gogol.com",+    "phone": "+1 (809) 411-2991",+    "address": "778 McKibben Street, Whitehaven, Louisiana, 2079",+    "about": "Cillum ipsum cupidatat et dolor minim aliqua. Laborum culpa magna ullamco nulla ut labore deserunt amet sunt est esse do. Consequat aliquip eiusmod ipsum tempor. Voluptate exercitation nisi nulla dolor esse ullamco occaecat. Sint minim reprehenderit anim tempor deserunt sit exercitation. Qui proident cillum eu voluptate adipisicing velit tempor sunt sunt ipsum. Enim anim ea ea eu excepteur excepteur laboris eu.\r\n",+    "registered": "2014-09-13T12:46:51 +07:00",+    "latitude": 81.172596,+    "longitude": -151.851605,+    "tags": [+      "velit",+      "ad",+      "tempor",+      "tempor",+      "adipisicing",+      "nulla",+      "minim"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Lester Woods"+      },+      {+        "id": 1,+        "name": "Audra Shepherd"+      },+      {+        "id": 2,+        "name": "Chandler Church"+      }+    ],+    "greeting": "Hello, Josefina Holland! You have 5 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a9008a3955509cf065f",+    "index": 34,+    "guid": "4c838bee-e8b1-4c22-b8ad-e0e5d0822334",+    "isActive": false,+    "balance": "$3,032.13",+    "picture": "http://placehold.it/32x32",+    "age": 32,+    "eyeColor": "green",+    "name": "Susie Cash",+    "gender": "female",+    "company": "DOGSPA",+    "email": "susiecash@dogspa.com",+    "phone": "+1 (991) 447-2298",+    "address": "361 Quincy Street, Duryea, Florida, 7538",+    "about": "Officia eu non proident ad ea quis eu ea amet irure aliqua qui. Ea in laborum do sit. Dolor proident voluptate excepteur sit. Tempor nulla anim ex proident ipsum laboris laboris cupidatat esse dolore ex fugiat proident. Excepteur exercitation pariatur ex velit. In adipisicing eu eiusmod elit dolore ullamco nulla sit ut ut anim aliquip. Et eu eu aliquip ex aute aliquip veniam sint pariatur non incididunt in aliqua sit.\r\n",+    "registered": "2014-09-14T22:55:19 +07:00",+    "latitude": 8.46119,+    "longitude": -1.171475,+    "tags": [+      "voluptate",+      "irure",+      "ex",+      "proident",+      "nulla",+      "in",+      "esse"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Cassie Whitley"+      },+      {+        "id": 1,+        "name": "Shawna Todd"+      },+      {+        "id": 2,+        "name": "Antoinette Brooks"+      }+    ],+    "greeting": "Hello, Susie Cash! You have 1 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a9079c5bce30e9c0a68",+    "index": 35,+    "guid": "7a70e0df-c23b-435d-8d19-864cc1bd09e6",+    "isActive": false,+    "balance": "$2,523.44",+    "picture": "http://placehold.it/32x32",+    "age": 21,+    "eyeColor": "brown",+    "name": "Marquez Hartman",+    "gender": "male",+    "company": "BUZZMAKER",+    "email": "marquezhartman@buzzmaker.com",+    "phone": "+1 (880) 574-2420",+    "address": "326 Church Avenue, Yardville, Indiana, 6743",+    "about": "Ullamco reprehenderit pariatur veniam enim est nulla laboris ullamco eiusmod eu Lorem amet in amet. Incididunt consectetur reprehenderit tempor Lorem aute eiusmod laborum sunt mollit excepteur qui ea enim. Consectetur enim excepteur proident incididunt adipisicing duis est amet quis proident laboris. Consectetur Lorem consequat incididunt eiusmod mollit id amet velit quis ipsum labore. Cupidatat id minim cillum qui consequat quis non quis irure dolor voluptate ea eu.\r\n",+    "registered": "2014-06-16T07:23:30 +07:00",+    "latitude": -88.970617,+    "longitude": -28.068887,+    "tags": [+      "nostrud",+      "adipisicing",+      "quis",+      "voluptate",+      "dolore",+      "dolore",+      "reprehenderit"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Florence David"+      },+      {+        "id": 1,+        "name": "Lynn Mcleod"+      },+      {+        "id": 2,+        "name": "Catherine Abbott"+      }+    ],+    "greeting": "Hello, Marquez Hartman! You have 10 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a902bf5baf83c4fafcd",+    "index": 36,+    "guid": "56ac4022-1ed3-4298-9b70-c6060c69b13c",+    "isActive": false,+    "balance": "$1,162.32",+    "picture": "http://placehold.it/32x32",+    "age": 35,+    "eyeColor": "green",+    "name": "Bishop Gibbs",+    "gender": "male",+    "company": "MICROLUXE",+    "email": "bishopgibbs@microluxe.com",+    "phone": "+1 (808) 584-2587",+    "address": "801 Exeter Street, Trucksville, New Hampshire, 4871",+    "about": "Incididunt sit eu sunt cupidatat aute do eu eu culpa. Laborum adipisicing sit anim sit dolor labore commodo minim proident ullamco laboris sit irure. Duis dolore cupidatat ullamco anim reprehenderit aliquip incididunt sunt eu id non proident non.\r\n",+    "registered": "2014-09-10T02:39:14 +07:00",+    "latitude": -3.268064,+    "longitude": 149.311591,+    "tags": [+      "enim",+      "aute",+      "commodo",+      "consectetur",+      "Lorem",+      "est",+      "cillum"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Head Pickett"+      },+      {+        "id": 1,+        "name": "Thornton Russo"+      },+      {+        "id": 2,+        "name": "Hooper Rivera"+      }+    ],+    "greeting": "Hello, Bishop Gibbs! You have 9 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a90a4406555b5bdab8f",+    "index": 37,+    "guid": "0f052dd2-283e-441e-86d7-b7f7b47224f1",+    "isActive": true,+    "balance": "$3,306.81",+    "picture": "http://placehold.it/32x32",+    "age": 34,+    "eyeColor": "brown",+    "name": "Carr Strickland",+    "gender": "male",+    "company": "EXERTA",+    "email": "carrstrickland@exerta.com",+    "phone": "+1 (862) 455-3845",+    "address": "730 Aberdeen Street, Klondike, Kansas, 4412",+    "about": "Pariatur velit qui Lorem cillum labore veniam minim. Nisi reprehenderit commodo consectetur commodo velit. Ipsum dolor ea mollit excepteur minim do excepteur. Aliquip cillum quis nostrud cillum duis eu laborum reprehenderit adipisicing adipisicing do exercitation. Laborum Lorem sunt voluptate qui quis officia nisi et elit sint veniam. Lorem velit ad do amet labore veniam consectetur.\r\n",+    "registered": "2014-01-19T10:53:58 +08:00",+    "latitude": -45.243563,+    "longitude": -17.8448,+    "tags": [+      "mollit",+      "ex",+      "qui",+      "fugiat",+      "consequat",+      "consectetur",+      "do"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Rosario Gomez"+      },+      {+        "id": 1,+        "name": "Robinson Robinson"+      },+      {+        "id": 2,+        "name": "Vicki Moreno"+      }+    ],+    "greeting": "Hello, Carr Strickland! You have 2 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a9097f6a585321fe001",+    "index": 38,+    "guid": "eee159c2-73ac-44b7-9054-d72295d3178a",+    "isActive": true,+    "balance": "$3,336.97",+    "picture": "http://placehold.it/32x32",+    "age": 30,+    "eyeColor": "brown",+    "name": "Stephens Harper",+    "gender": "male",+    "company": "KIDSTOCK",+    "email": "stephensharper@kidstock.com",+    "phone": "+1 (989) 464-3887",+    "address": "315 Hewes Street, Cuylerville, Pennsylvania, 7331",+    "about": "Et ea Lorem labore consequat laboris. Aliqua irure elit aliqua ad fugiat. Commodo cupidatat incididunt ut nostrud consequat elit ad esse magna nisi nostrud dolore eu. Minim incididunt esse ullamco esse ullamco deserunt reprehenderit commodo ut. Quis sint cupidatat excepteur et laboris incididunt id esse irure nostrud sint nostrud ut pariatur. Pariatur officia aute ullamco laboris nisi elit quis labore Lorem minim ad. Ea esse anim commodo fugiat exercitation voluptate eiusmod aliquip amet enim nulla mollit excepteur nisi.\r\n",+    "registered": "2014-03-02T06:45:25 +08:00",+    "latitude": -88.39292,+    "longitude": -121.170605,+    "tags": [+      "ullamco",+      "est",+      "mollit",+      "id",+      "commodo",+      "Lorem",+      "nostrud"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Castaneda Dillon"+      },+      {+        "id": 1,+        "name": "Morin Hodge"+      },+      {+        "id": 2,+        "name": "Vincent Lamb"+      }+    ],+    "greeting": "Hello, Stephens Harper! You have 2 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a90357d07212fdefd7a",+    "index": 39,+    "guid": "160a80a4-8e5d-483a-93ed-0a37ee359f04",+    "isActive": true,+    "balance": "$1,588.64",+    "picture": "http://placehold.it/32x32",+    "age": 32,+    "eyeColor": "green",+    "name": "Espinoza May",+    "gender": "male",+    "company": "ZIDOX",+    "email": "espinozamay@zidox.com",+    "phone": "+1 (955) 420-2244",+    "address": "614 Alice Court, Vienna, Delaware, 5018",+    "about": "Pariatur voluptate culpa enim enim sunt dolor laborum consequat officia est nostrud. Ad pariatur labore reprehenderit cillum dolor consequat quis eu fugiat. Ut fugiat do dolor minim voluptate incididunt laboris magna deserunt. Culpa incididunt fugiat nostrud qui incididunt velit laboris deserunt exercitation velit.\r\n",+    "registered": "2014-09-16T06:10:45 +07:00",+    "latitude": -28.413217,+    "longitude": -76.45198,+    "tags": [+      "ullamco",+      "ut",+      "nulla",+      "consequat",+      "ea",+      "in",+      "commodo"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Virginia Obrien"+      },+      {+        "id": 1,+        "name": "Snider Hopper"+      },+      {+        "id": 2,+        "name": "Fannie Harrell"+      }+    ],+    "greeting": "Hello, Espinoza May! You have 3 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a901e753e41d1232a42",+    "index": 40,+    "guid": "7f3923e7-2fad-4210-b051-bd805e505d06",+    "isActive": false,+    "balance": "$2,512.14",+    "picture": "http://placehold.it/32x32",+    "age": 28,+    "eyeColor": "green",+    "name": "Morrison Guerrero",+    "gender": "male",+    "company": "MARQET",+    "email": "morrisonguerrero@marqet.com",+    "phone": "+1 (881) 456-3335",+    "address": "157 Huntington Street, Topaz, Georgia, 6068",+    "about": "Velit cupidatat eiusmod elit ut adipisicing. Proident pariatur consequat ullamco velit dolore do esse adipisicing non est ea. Velit non amet nisi aliqua est veniam dolor. Cillum id labore qui ut aute pariatur exercitation ut incididunt velit aute.\r\n",+    "registered": "2014-11-12T21:01:15 +08:00",+    "latitude": -43.420946,+    "longitude": 23.215515,+    "tags": [+      "sit",+      "dolore",+      "aliqua",+      "esse",+      "et",+      "cillum",+      "mollit"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Pamela Stanton"+      },+      {+        "id": 1,+        "name": "Spence Banks"+      },+      {+        "id": 2,+        "name": "Newton Cooley"+      }+    ],+    "greeting": "Hello, Morrison Guerrero! You have 4 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a9098c06d55fa03398f",+    "index": 41,+    "guid": "c17e4f56-ca2b-48fd-bbc4-aea053062ebd",+    "isActive": false,+    "balance": "$3,405.25",+    "picture": "http://placehold.it/32x32",+    "age": 26,+    "eyeColor": "brown",+    "name": "Janna Porter",+    "gender": "female",+    "company": "NIMON",+    "email": "jannaporter@nimon.com",+    "phone": "+1 (974) 475-2497",+    "address": "231 Wythe Place, Stonybrook, Alaska, 6291",+    "about": "Veniam aute dolor Lorem dolore enim eiusmod veniam esse pariatur duis nisi. Eu sit sunt in exercitation amet veniam reprehenderit voluptate proident officia. Dolore aliquip laboris veniam cupidatat non cupidatat ipsum esse labore Lorem reprehenderit ex. Amet magna do officia proident laborum laboris enim. Ea sunt quis officia minim ea in id cillum. Aute ipsum aliquip fugiat tempor proident sit pariatur duis fugiat fugiat esse cillum eiusmod laboris.\r\n",+    "registered": "2014-08-21T22:08:24 +07:00",+    "latitude": -43.52587,+    "longitude": 143.40506,+    "tags": [+      "ipsum",+      "consequat",+      "labore",+      "tempor",+      "ipsum",+      "exercitation",+      "sint"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Bray Francis"+      },+      {+        "id": 1,+        "name": "Whitley Guthrie"+      },+      {+        "id": 2,+        "name": "Christa Short"+      }+    ],+    "greeting": "Hello, Janna Porter! You have 1 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a90ddf3f9e967357339",+    "index": 42,+    "guid": "5b194b7d-c5ca-4cdd-9239-6aa6f9b28e2f",+    "isActive": false,+    "balance": "$1,601.96",+    "picture": "http://placehold.it/32x32",+    "age": 31,+    "eyeColor": "green",+    "name": "Sandoval Melton",+    "gender": "male",+    "company": "GENMEX",+    "email": "sandovalmelton@genmex.com",+    "phone": "+1 (826) 536-3504",+    "address": "726 Lester Court, Gracey, Colorado, 4092",+    "about": "Fugiat pariatur elit Lorem amet fugiat tempor. Quis sint fugiat officia deserunt voluptate fugiat aliquip reprehenderit est laborum. Ut id ullamco Lorem esse occaecat eu officia veniam velit. Aliquip nisi voluptate id aliquip qui tempor commodo occaecat velit adipisicing nisi cillum id adipisicing.\r\n",+    "registered": "2014-09-05T08:54:55 +07:00",+    "latitude": -66.159672,+    "longitude": -135.605108,+    "tags": [+      "deserunt",+      "minim",+      "nulla",+      "ea",+      "tempor",+      "reprehenderit",+      "nulla"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Veronica Goodwin"+      },+      {+        "id": 1,+        "name": "Heather Pena"+      },+      {+        "id": 2,+        "name": "Lynch Crane"+      }+    ],+    "greeting": "Hello, Sandoval Melton! You have 2 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a904d114bb57f0826af",+    "index": 43,+    "guid": "57e4715d-f7e7-4843-9844-638648315628",+    "isActive": false,+    "balance": "$1,820.21",+    "picture": "http://placehold.it/32x32",+    "age": 34,+    "eyeColor": "blue",+    "name": "Flynn Fitzgerald",+    "gender": "male",+    "company": "ORBEAN",+    "email": "flynnfitzgerald@orbean.com",+    "phone": "+1 (931) 578-2443",+    "address": "662 Degraw Street, Morgandale, Alabama, 5945",+    "about": "Nulla anim qui duis ea do consectetur eiusmod commodo. Minim sunt aliqua velit ullamco velit dolor in amet. Eiusmod Lorem sint ea ea nisi ullamco ut officia magna id adipisicing. Ut eu minim pariatur incididunt non nisi in nostrud culpa magna anim. Veniam dolor consequat cillum sunt elit.\r\n",+    "registered": "2014-06-21T08:11:23 +07:00",+    "latitude": 17.108863,+    "longitude": -21.608292,+    "tags": [+      "officia",+      "quis",+      "sint",+      "ipsum",+      "esse",+      "commodo",+      "aute"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Browning Mcdonald"+      },+      {+        "id": 1,+        "name": "Austin Lane"+      },+      {+        "id": 2,+        "name": "Blankenship Ferrell"+      }+    ],+    "greeting": "Hello, Flynn Fitzgerald! You have 6 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a903a4a0629f547f5ab",+    "index": 44,+    "guid": "1e128e33-65bf-4420-ae93-f228d030401c",+    "isActive": false,+    "balance": "$3,158.96",+    "picture": "http://placehold.it/32x32",+    "age": 25,+    "eyeColor": "brown",+    "name": "Holden Golden",+    "gender": "male",+    "company": "QABOOS",+    "email": "holdengolden@qaboos.com",+    "phone": "+1 (825) 535-2422",+    "address": "892 Vandervoort Place, Hessville, Guam, 3942",+    "about": "Non sunt cupidatat ipsum nostrud qui ad ex nostrud fugiat laboris minim. Aliquip dolore est proident aliquip in non culpa. Minim ut laboris duis proident commodo occaecat duis duis cupidatat incididunt aliqua. Ad qui anim non occaecat excepteur labore consequat Lorem.\r\n",+    "registered": "2014-09-11T00:33:16 +07:00",+    "latitude": -3.274271,+    "longitude": -151.539207,+    "tags": [+      "nostrud",+      "non",+      "sint",+      "laborum",+      "deserunt",+      "velit",+      "sit"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Barbara Sharpe"+      },+      {+        "id": 1,+        "name": "Jerri Martinez"+      },+      {+        "id": 2,+        "name": "Ronda Montoya"+      }+    ],+    "greeting": "Hello, Holden Golden! You have 3 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a9079e20c148518a60a",+    "index": 45,+    "guid": "90fa284f-6cba-42d4-8484-6edb1ffc260b",+    "isActive": false,+    "balance": "$1,739.35",+    "picture": "http://placehold.it/32x32",+    "age": 27,+    "eyeColor": "blue",+    "name": "Maxwell Hodges",+    "gender": "male",+    "company": "DOGTOWN",+    "email": "maxwellhodges@dogtown.com",+    "phone": "+1 (962) 527-3585",+    "address": "696 Baughman Place, Cucumber, South Carolina, 5454",+    "about": "Officia ea in non veniam ipsum ea magna enim velit. Enim mollit duis consectetur magna fugiat nostrud cillum duis. Voluptate qui nostrud amet ea eiusmod consectetur cupidatat fugiat in eiusmod amet esse excepteur aute. Magna qui duis ex qui irure esse amet. Quis amet ex esse aliquip mollit. Deserunt consectetur eiusmod culpa reprehenderit. Duis quis excepteur non in consectetur commodo.\r\n",+    "registered": "2014-03-20T11:37:19 +07:00",+    "latitude": 19.760315,+    "longitude": -26.731699,+    "tags": [+      "magna",+      "aute",+      "consectetur",+      "labore",+      "sunt",+      "pariatur",+      "excepteur"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Violet Vincent"+      },+      {+        "id": 1,+        "name": "West Dorsey"+      },+      {+        "id": 2,+        "name": "Concepcion Hull"+      }+    ],+    "greeting": "Hello, Maxwell Hodges! You have 3 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a903c504d648884f367",+    "index": 46,+    "guid": "7fd30275-a097-41e0-8924-252f0a14d6ba",+    "isActive": false,+    "balance": "$1,153.05",+    "picture": "http://placehold.it/32x32",+    "age": 22,+    "eyeColor": "brown",+    "name": "Chasity Day",+    "gender": "female",+    "company": "EQUITAX",+    "email": "chasityday@equitax.com",+    "phone": "+1 (900) 484-3047",+    "address": "858 Gelston Avenue, Succasunna, Arkansas, 3818",+    "about": "Est ullamco deserunt cillum enim velit velit magna qui nisi eiusmod enim officia sit voluptate. Laboris laborum elit nostrud sit in elit labore occaecat mollit Lorem. Aliquip ipsum dolore cupidatat ad anim sint non. Commodo ut fugiat ut dolor irure id velit enim in Lorem.\r\n",+    "registered": "2014-01-05T08:26:01 +08:00",+    "latitude": -34.705083,+    "longitude": -77.213981,+    "tags": [+      "consectetur",+      "velit",+      "enim",+      "excepteur",+      "ipsum",+      "non",+      "adipisicing"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Judy Trevino"+      },+      {+        "id": 1,+        "name": "Rosie Cervantes"+      },+      {+        "id": 2,+        "name": "Stella Cortez"+      }+    ],+    "greeting": "Hello, Chasity Day! You have 1 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a90ac6406c3b9606052",+    "index": 47,+    "guid": "9bcd39b8-10db-40cc-a9f0-73e8179f181b",+    "isActive": true,+    "balance": "$1,755.70",+    "picture": "http://placehold.it/32x32",+    "age": 21,+    "eyeColor": "green",+    "name": "Richards Mcfadden",+    "gender": "male",+    "company": "ZILLACOM",+    "email": "richardsmcfadden@zillacom.com",+    "phone": "+1 (883) 447-2941",+    "address": "241 Turnbull Avenue, Chesterfield, Ohio, 7594",+    "about": "Fugiat ut qui qui et Lorem ipsum do anim veniam. Sunt ex Lorem et nostrud consectetur esse nostrud qui minim ut veniam commodo. Cillum enim laboris aliquip sunt quis ut et adipisicing deserunt. Est aute amet elit non duis deserunt nisi quis excepteur do velit. Nostrud laboris ullamco sunt occaecat Lorem nostrud laboris.\r\n",+    "registered": "2014-07-04T03:08:44 +07:00",+    "latitude": -66.813737,+    "longitude": 75.337073,+    "tags": [+      "elit",+      "consequat",+      "excepteur",+      "qui",+      "deserunt",+      "sint",+      "ipsum"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Mercado Quinn"+      },+      {+        "id": 1,+        "name": "Lucia Roberts"+      },+      {+        "id": 2,+        "name": "Essie Cohen"+      }+    ],+    "greeting": "Hello, Richards Mcfadden! You have 5 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a9033409e09e42cf3f1",+    "index": 48,+    "guid": "6ea51613-14f0-42b7-97c6-eb7a5cc32226",+    "isActive": true,+    "balance": "$3,907.64",+    "picture": "http://placehold.it/32x32",+    "age": 31,+    "eyeColor": "blue",+    "name": "Sargent Harrington",+    "gender": "male",+    "company": "DIGINETIC",+    "email": "sargentharrington@diginetic.com",+    "phone": "+1 (974) 427-2742",+    "address": "883 Kermit Place, Rehrersburg, Virgin Islands, 4044",+    "about": "Officia eiusmod do ut ipsum voluptate aute mollit minim ea incididunt ipsum commodo. Qui aute commodo dolor id pariatur ullamco adipisicing duis irure reprehenderit aliquip ullamco consequat. Sunt ad non deserunt velit consectetur ipsum. Sit voluptate aliquip nisi ea pariatur proident ut eu cillum. Irure sunt ad aliqua et enim. Deserunt deserunt incididunt Lorem commodo aliqua.\r\n",+    "registered": "2014-06-16T00:32:02 +07:00",+    "latitude": -88.301682,+    "longitude": -16.268399,+    "tags": [+      "Lorem",+      "reprehenderit",+      "consequat",+      "officia",+      "velit",+      "ad",+      "ex"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Dolly Swanson"+      },+      {+        "id": 1,+        "name": "Jeannette Underwood"+      },+      {+        "id": 2,+        "name": "Ayers Wheeler"+      }+    ],+    "greeting": "Hello, Sargent Harrington! You have 3 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a9083fe43074bb5b9c8",+    "index": 49,+    "guid": "d5835fea-0882-4e20-b86e-6dd8b9534b49",+    "isActive": false,+    "balance": "$2,481.41",+    "picture": "http://placehold.it/32x32",+    "age": 22,+    "eyeColor": "green",+    "name": "Kristin Perez",+    "gender": "female",+    "company": "JAMNATION",+    "email": "kristinperez@jamnation.com",+    "phone": "+1 (917) 575-2693",+    "address": "737 Schroeders Avenue, Chemung, Nebraska, 8601",+    "about": "Voluptate id id mollit in aliquip aliquip duis cupidatat elit voluptate proident sit sunt voluptate. Irure ipsum esse duis culpa esse et do officia consequat velit commodo Lorem. Dolore incididunt proident proident officia aliqua mollit nisi exercitation consectetur. Est dolore consectetur aliqua occaecat est. Labore nostrud ipsum aliquip labore elit aliquip magna ex esse do consequat minim id. Sit sint ea ad irure voluptate occaecat duis magna. Sint ex minim irure anim sint do cillum.\r\n",+    "registered": "2014-08-12T23:35:39 +07:00",+    "latitude": 75.002428,+    "longitude": 12.266159,+    "tags": [+      "anim",+      "exercitation",+      "excepteur",+      "amet",+      "aute",+      "mollit",+      "aliquip"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Gilliam Steele"+      },+      {+        "id": 1,+        "name": "Loretta Brennan"+      },+      {+        "id": 2,+        "name": "Ratliff Norton"+      }+    ],+    "greeting": "Hello, Kristin Perez! You have 9 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a90f594e03626363339",+    "index": 50,+    "guid": "bdb8284c-7b55-4791-90c0-d9e4abecd91f",+    "isActive": true,+    "balance": "$1,906.85",+    "picture": "http://placehold.it/32x32",+    "age": 33,+    "eyeColor": "green",+    "name": "Petra Scott",+    "gender": "female",+    "company": "BRAINCLIP",+    "email": "petrascott@brainclip.com",+    "phone": "+1 (942) 449-2471",+    "address": "759 Coleridge Street, Tuskahoma, Virginia, 9943",+    "about": "Consectetur magna consequat ea est. Minim veniam culpa deserunt aliqua in dolor ex aliqua occaecat reprehenderit. Proident in mollit ad est excepteur anim quis aliquip non consequat minim cillum deserunt. Occaecat sint ad et irure sit ad ad sit sint quis sunt nisi.\r\n",+    "registered": "2014-11-12T20:54:05 +08:00",+    "latitude": -64.192199,+    "longitude": -144.535209,+    "tags": [+      "cillum",+      "id",+      "aliqua",+      "reprehenderit",+      "officia",+      "aliquip",+      "in"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Holmes Rasmussen"+      },+      {+        "id": 1,+        "name": "Leach Hickman"+      },+      {+        "id": 2,+        "name": "Noelle Battle"+      }+    ],+    "greeting": "Hello, Petra Scott! You have 9 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a902f79c4cdada83391",+    "index": 51,+    "guid": "c79aaabd-212b-4dc8-b1b5-de14362b550c",+    "isActive": true,+    "balance": "$2,845.25",+    "picture": "http://placehold.it/32x32",+    "age": 32,+    "eyeColor": "brown",+    "name": "Kelli Gates",+    "gender": "female",+    "company": "MUSANPOLY",+    "email": "kelligates@musanpoly.com",+    "phone": "+1 (899) 414-3276",+    "address": "888 Elm Place, Grayhawk, Wyoming, 8032",+    "about": "Nisi excepteur laboris duis magna dolore occaecat dolor cupidatat. Ut commodo proident incididunt ea anim enim deserunt laboris occaecat anim cillum. In amet exercitation officia mollit officia quis exercitation aliqua culpa ullamco consequat excepteur aute mollit. Aliqua tempor consequat ullamco consequat qui duis tempor ex est aliqua id nulla Lorem occaecat. Et laborum culpa pariatur et ad minim. Nulla ad eiusmod pariatur velit aliquip magna duis duis tempor. Nisi ea reprehenderit aliquip magna esse occaecat consectetur ullamco esse.\r\n",+    "registered": "2014-03-01T15:11:58 +08:00",+    "latitude": 30.679655,+    "longitude": -175.200369,+    "tags": [+      "laborum",+      "non",+      "pariatur",+      "ea",+      "non",+      "proident",+      "culpa"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Coleman Mejia"+      },+      {+        "id": 1,+        "name": "Meyers Sellers"+      },+      {+        "id": 2,+        "name": "Stuart Carney"+      }+    ],+    "greeting": "Hello, Kelli Gates! You have 1 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a9040ef999266e0142c",+    "index": 52,+    "guid": "cb9bc903-5e95-4918-91ed-8485d75489fa",+    "isActive": true,+    "balance": "$1,352.24",+    "picture": "http://placehold.it/32x32",+    "age": 29,+    "eyeColor": "green",+    "name": "Maricela Conrad",+    "gender": "female",+    "company": "QUIZKA",+    "email": "maricelaconrad@quizka.com",+    "phone": "+1 (924) 592-2199",+    "address": "339 Virginia Place, Lutsen, Marshall Islands, 1479",+    "about": "Quis commodo cupidatat mollit et deserunt id ut anim nostrud nisi mollit esse commodo laborum. Cillum exercitation anim cillum esse quis consequat. Dolor laboris exercitation irure ullamco excepteur ipsum deserunt. Anim veniam dolore labore exercitation et consectetur eiusmod eu. Aliquip velit consectetur nostrud dolore.\r\n",+    "registered": "2014-06-15T22:10:22 +07:00",+    "latitude": -0.554318,+    "longitude": -65.103334,+    "tags": [+      "adipisicing",+      "irure",+      "deserunt",+      "culpa",+      "cupidatat",+      "do",+      "occaecat"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Gentry Holden"+      },+      {+        "id": 1,+        "name": "Dillard Herrera"+      },+      {+        "id": 2,+        "name": "Guzman Patton"+      }+    ],+    "greeting": "Hello, Maricela Conrad! You have 1 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a905369b0bb27f85607",+    "index": 53,+    "guid": "fa3a125f-2d10-4015-a0fd-68d7e2401a31",+    "isActive": false,+    "balance": "$2,858.28",+    "picture": "http://placehold.it/32x32",+    "age": 34,+    "eyeColor": "green",+    "name": "Simpson Cleveland",+    "gender": "male",+    "company": "ZAPPIX",+    "email": "simpsoncleveland@zappix.com",+    "phone": "+1 (808) 572-3724",+    "address": "202 Georgia Avenue, Jamestown, Texas, 6347",+    "about": "Reprehenderit elit in duis sunt quis consequat mollit ut consequat amet. Incididunt non sint culpa non mollit incididunt elit irure sint excepteur esse sit ut fugiat. Ullamco esse culpa excepteur magna laborum sunt magna ut reprehenderit aute laboris ullamco Lorem ea. Culpa ut sit amet dolor adipisicing veniam non occaecat ex ad ea. Quis nostrud sit mollit aliqua adipisicing.\r\n",+    "registered": "2014-10-27T07:51:50 +07:00",+    "latitude": -34.024135,+    "longitude": 66.799767,+    "tags": [+      "anim",+      "pariatur",+      "fugiat",+      "reprehenderit",+      "ullamco",+      "labore",+      "fugiat"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Hester Howell"+      },+      {+        "id": 1,+        "name": "Deena King"+      },+      {+        "id": 2,+        "name": "Wilkerson Fox"+      }+    ],+    "greeting": "Hello, Simpson Cleveland! You have 10 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a900271aa8695bc5cdf",+    "index": 54,+    "guid": "501e7876-6b3f-4f2e-bbaf-914e6ff23845",+    "isActive": false,+    "balance": "$2,015.78",+    "picture": "http://placehold.it/32x32",+    "age": 23,+    "eyeColor": "brown",+    "name": "Queen Mckay",+    "gender": "female",+    "company": "PROXSOFT",+    "email": "queenmckay@proxsoft.com",+    "phone": "+1 (954) 439-2402",+    "address": "749 Wakeman Place, Sidman, Massachusetts, 3345",+    "about": "Culpa irure velit cupidatat voluptate ad non anim sit sunt in esse adipisicing. Officia amet Lorem et id eiusmod non commodo deserunt irure tempor aute fugiat est cupidatat. Ut amet ad consequat nulla in in deserunt minim ipsum adipisicing eu adipisicing nisi nulla. Qui culpa sunt aute amet est consequat excepteur occaecat voluptate in do. In labore aute sit duis enim sunt qui ullamco est tempor labore aute aliquip. Sunt eu veniam in amet cillum nisi tempor quis in fugiat culpa exercitation sunt. Exercitation enim dolore sit exercitation in mollit culpa magna amet et amet.\r\n",+    "registered": "2014-05-02T07:58:52 +07:00",+    "latitude": -24.836277,+    "longitude": -108.207588,+    "tags": [+      "aliquip",+      "dolor",+      "velit",+      "non",+      "sint",+      "deserunt",+      "ut"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Maynard Shannon"+      },+      {+        "id": 1,+        "name": "Ernestine Sullivan"+      },+      {+        "id": 2,+        "name": "Sullivan Juarez"+      }+    ],+    "greeting": "Hello, Queen Mckay! You have 8 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a902c9d0c4f7379c125",+    "index": 55,+    "guid": "c13147c1-9aab-432b-9eaa-eda127454f06",+    "isActive": false,+    "balance": "$1,498.79",+    "picture": "http://placehold.it/32x32",+    "age": 21,+    "eyeColor": "green",+    "name": "Madeline Rhodes",+    "gender": "female",+    "company": "LOCAZONE",+    "email": "madelinerhodes@locazone.com",+    "phone": "+1 (817) 404-2650",+    "address": "648 Rochester Avenue, Tetherow, Arizona, 751",+    "about": "Sit velit ea enim mollit ad fugiat anim commodo minim qui sit dolor aliqua. Amet nostrud aliquip qui velit et ea reprehenderit sint ipsum quis. Ut amet aute enim proident. Et officia nostrud voluptate non tempor quis aliquip ullamco commodo consequat fugiat ut. Eiusmod sit excepteur non nostrud esse consequat nulla sunt deserunt aliqua. Commodo laborum voluptate ea eu non excepteur pariatur.\r\n",+    "registered": "2014-03-14T19:50:42 +07:00",+    "latitude": -31.489199,+    "longitude": 124.798218,+    "tags": [+      "tempor",+      "culpa",+      "pariatur",+      "irure",+      "nisi",+      "proident",+      "quis"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Hebert Kline"+      },+      {+        "id": 1,+        "name": "Daisy Cochran"+      },+      {+        "id": 2,+        "name": "Russell Gilmore"+      }+    ],+    "greeting": "Hello, Madeline Rhodes! You have 10 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a90b6d8621dab6c9126",+    "index": 56,+    "guid": "2ebcecc1-180e-46b7-bdca-9d9a6babc687",+    "isActive": false,+    "balance": "$3,190.82",+    "picture": "http://placehold.it/32x32",+    "age": 27,+    "eyeColor": "blue",+    "name": "Rasmussen Knapp",+    "gender": "male",+    "company": "BIOSPAN",+    "email": "rasmussenknapp@biospan.com",+    "phone": "+1 (860) 461-3219",+    "address": "214 Williams Court, Brady, Oregon, 5174",+    "about": "Tempor ullamco officia laborum irure cillum est culpa tempor pariatur aliqua officia incididunt tempor. Laboris adipisicing nostrud Lorem veniam enim consequat mollit nisi. Minim nulla culpa dolor esse dolore deserunt reprehenderit adipisicing aliquip.\r\n",+    "registered": "2014-08-11T17:15:51 +07:00",+    "latitude": -25.850361,+    "longitude": -36.01581,+    "tags": [+      "enim",+      "laboris",+      "dolore",+      "amet",+      "nisi",+      "in",+      "aliqua"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Letha Peters"+      },+      {+        "id": 1,+        "name": "Lavonne Mccarthy"+      },+      {+        "id": 2,+        "name": "Hensley Kim"+      }+    ],+    "greeting": "Hello, Rasmussen Knapp! You have 5 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a902e4d6731ddf406bf",+    "index": 57,+    "guid": "7a13b0b9-7cc4-477a-944f-a27eb7e74ae6",+    "isActive": false,+    "balance": "$1,711.06",+    "picture": "http://placehold.it/32x32",+    "age": 21,+    "eyeColor": "brown",+    "name": "Maryellen Russell",+    "gender": "female",+    "company": "MAXEMIA",+    "email": "maryellenrussell@maxemia.com",+    "phone": "+1 (839) 590-2573",+    "address": "514 Roosevelt Court, Sugartown, American Samoa, 6602",+    "about": "Pariatur laborum nostrud aliqua nisi cillum sunt laborum cillum minim pariatur et consectetur minim adipisicing. Exercitation non dolor id voluptate aute sint sunt. Nostrud cupidatat officia dolor cupidatat ex irure non. Fugiat laboris quis id aliqua sunt consectetur est ea. Eiusmod mollit enim Lorem cupidatat consectetur anim nostrud enim eu irure. Et eiusmod velit esse fugiat elit laboris labore voluptate ipsum. Excepteur consequat proident quis voluptate excepteur nulla in.\r\n",+    "registered": "2014-05-17T21:19:21 +07:00",+    "latitude": -15.634102,+    "longitude": -81.860909,+    "tags": [+      "ea",+      "ipsum",+      "esse",+      "veniam",+      "reprehenderit",+      "adipisicing",+      "do"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Virgie Whitaker"+      },+      {+        "id": 1,+        "name": "Earlene Ruiz"+      },+      {+        "id": 2,+        "name": "Rosa Estrada"+      }+    ],+    "greeting": "Hello, Maryellen Russell! You have 5 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a906ea1b53a8de569dd",+    "index": 58,+    "guid": "0e91224f-af4a-4cb0-a51e-5874ad140b32",+    "isActive": false,+    "balance": "$1,226.26",+    "picture": "http://placehold.it/32x32",+    "age": 21,+    "eyeColor": "blue",+    "name": "Wright Morris",+    "gender": "male",+    "company": "APPLICA",+    "email": "wrightmorris@applica.com",+    "phone": "+1 (968) 550-2677",+    "address": "630 Hornell Loop, Sparkill, Montana, 7355",+    "about": "Veniam commodo nostrud nostrud ullamco do aliquip quis dolore laboris culpa minim do est. In sunt consequat irure veniam minim duis ex commodo duis. Ullamco eu reprehenderit dolore eiusmod est exercitation nisi tempor.\r\n",+    "registered": "2014-02-05T10:33:53 +08:00",+    "latitude": -16.050009,+    "longitude": -149.883606,+    "tags": [+      "Lorem",+      "in",+      "est",+      "consequat",+      "cillum",+      "adipisicing",+      "commodo"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Washington Carson"+      },+      {+        "id": 1,+        "name": "Hines Bean"+      },+      {+        "id": 2,+        "name": "Cox Powell"+      }+    ],+    "greeting": "Hello, Wright Morris! You have 7 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a9047deb3b6b5122221",+    "index": 59,+    "guid": "f85bae2d-b72a-4f81-b596-b5338ec41578",+    "isActive": true,+    "balance": "$1,928.94",+    "picture": "http://placehold.it/32x32",+    "age": 26,+    "eyeColor": "brown",+    "name": "Agnes Guzman",+    "gender": "female",+    "company": "PEARLESSA",+    "email": "agnesguzman@pearlessa.com",+    "phone": "+1 (859) 451-3816",+    "address": "874 Duffield Street, Benson, Hawaii, 1388",+    "about": "Ad voluptate incididunt ullamco ullamco velit adipisicing culpa aute minim adipisicing non. Consectetur elit ullamco mollit ipsum aliqua in enim Lorem eiusmod nisi fugiat exercitation. Do amet pariatur deserunt ex labore exercitation non ad commodo aliquip. Nostrud magna ipsum reprehenderit qui consectetur cillum irure nostrud do quis excepteur cillum.\r\n",+    "registered": "2014-07-28T12:22:31 +07:00",+    "latitude": -89.041016,+    "longitude": -4.513498,+    "tags": [+      "eiusmod",+      "deserunt",+      "deserunt",+      "eiusmod",+      "minim",+      "nisi",+      "magna"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Kari Trujillo"+      },+      {+        "id": 1,+        "name": "Kaye Sloan"+      },+      {+        "id": 2,+        "name": "Aguilar Fields"+      }+    ],+    "greeting": "Hello, Agnes Guzman! You have 9 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a905ad56237e1e3ae99",+    "index": 60,+    "guid": "3244774e-3c8d-4c84-8ae6-d04486ce05d7",+    "isActive": false,+    "balance": "$3,045.23",+    "picture": "http://placehold.it/32x32",+    "age": 27,+    "eyeColor": "green",+    "name": "Jill Evans",+    "gender": "female",+    "company": "GOLISTIC",+    "email": "jillevans@golistic.com",+    "phone": "+1 (871) 577-2459",+    "address": "738 Putnam Avenue, Ona, South Dakota, 4749",+    "about": "Irure exercitation esse quis commodo sunt laborum esse id anim. Ut ut ad quis qui id labore ut tempor labore velit laborum pariatur. Dolore ipsum ea tempor excepteur esse cillum sit non eu sit irure adipisicing mollit. Nulla ut proident velit consequat officia magna ex duis proident aliqua amet exercitation do commodo. Nulla Lorem ad reprehenderit mollit deserunt veniam qui duis enim enim consectetur do.\r\n",+    "registered": "2014-05-05T11:50:14 +07:00",+    "latitude": -56.941561,+    "longitude": -169.158104,+    "tags": [+      "consequat",+      "minim",+      "minim",+      "laborum",+      "aute",+      "nisi",+      "aliqua"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Compton Becker"+      },+      {+        "id": 1,+        "name": "Conner Wall"+      },+      {+        "id": 2,+        "name": "Bright Harvey"+      }+    ],+    "greeting": "Hello, Jill Evans! You have 2 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a90f530d8bc9e6faf86",+    "index": 61,+    "guid": "a983ed9d-fb1b-48ee-94b7-54b33634ccbc",+    "isActive": false,+    "balance": "$3,091.12",+    "picture": "http://placehold.it/32x32",+    "age": 21,+    "eyeColor": "brown",+    "name": "Susana Berry",+    "gender": "female",+    "company": "POWERNET",+    "email": "susanaberry@powernet.com",+    "phone": "+1 (879) 454-3173",+    "address": "141 Cambridge Place, Florence, District Of Columbia, 6760",+    "about": "Magna occaecat laborum voluptate excepteur. Proident esse voluptate enim est exercitation elit voluptate minim ex. Velit officia cupidatat sit quis reprehenderit minim voluptate et nulla cupidatat duis mollit. Aute incididunt id magna nostrud cupidatat qui sit. Enim anim enim excepteur est sunt non enim proident sit non deserunt et veniam culpa. Ullamco irure ullamco id esse exercitation anim et fugiat labore. Do enim exercitation deserunt sint consectetur occaecat commodo commodo elit.\r\n",+    "registered": "2014-06-08T14:24:28 +07:00",+    "latitude": 75.590221,+    "longitude": -127.23641,+    "tags": [+      "esse",+      "anim",+      "est",+      "anim",+      "esse",+      "adipisicing",+      "mollit"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Helene Holcomb"+      },+      {+        "id": 1,+        "name": "Kramer Gregory"+      },+      {+        "id": 2,+        "name": "Fitzgerald Graves"+      }+    ],+    "greeting": "Hello, Susana Berry! You have 1 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a90e6f9c07f205357ec",+    "index": 62,+    "guid": "26cecce2-1202-4703-8bf5-8ff431336b37",+    "isActive": false,+    "balance": "$3,437.96",+    "picture": "http://placehold.it/32x32",+    "age": 33,+    "eyeColor": "blue",+    "name": "Weeks Wallace",+    "gender": "male",+    "company": "SUREMAX",+    "email": "weekswallace@suremax.com",+    "phone": "+1 (946) 528-2733",+    "address": "219 Dekalb Avenue, Hasty, North Carolina, 7278",+    "about": "Magna dolor ex dolor eiusmod sint laboris consequat non magna ipsum. Nostrud qui aute ut consectetur in amet deserunt aliqua voluptate. In velit nisi voluptate amet consequat. Tempor deserunt velit ut aliqua duis cupidatat laboris amet in ipsum aliquip adipisicing. Quis anim cillum labore ipsum commodo deserunt mollit tempor tempor pariatur dolore. Irure aute ipsum do occaecat laborum eiusmod ad proident exercitation elit cillum do.\r\n",+    "registered": "2014-07-17T22:33:55 +07:00",+    "latitude": -22.986103,+    "longitude": -110.848014,+    "tags": [+      "ut",+      "irure",+      "elit",+      "quis",+      "ut",+      "fugiat",+      "sunt"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Colon Tyler"+      },+      {+        "id": 1,+        "name": "Dianne Lucas"+      },+      {+        "id": 2,+        "name": "Mathews Acosta"+      }+    ],+    "greeting": "Hello, Weeks Wallace! You have 8 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a9034039bde61fe5fdb",+    "index": 63,+    "guid": "cad69671-eb0a-4128-8024-9a0f469cf1f8",+    "isActive": true,+    "balance": "$2,503.03",+    "picture": "http://placehold.it/32x32",+    "age": 21,+    "eyeColor": "green",+    "name": "Watson Tucker",+    "gender": "male",+    "company": "IMKAN",+    "email": "watsontucker@imkan.com",+    "phone": "+1 (841) 499-3837",+    "address": "297 Falmouth Street, Woodlands, Tennessee, 2227",+    "about": "Do aliquip laboris voluptate aliqua duis duis qui. Lorem officia consectetur et in nulla do aliquip occaecat labore officia sunt ipsum ad consequat. Culpa voluptate adipisicing dolore do velit ea.\r\n",+    "registered": "2014-09-16T17:39:32 +07:00",+    "latitude": 76.336221,+    "longitude": -154.036452,+    "tags": [+      "veniam",+      "non",+      "aliquip",+      "ex",+      "esse",+      "sint",+      "voluptate"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Lolita Finch"+      },+      {+        "id": 1,+        "name": "Gilmore Solis"+      },+      {+        "id": 2,+        "name": "Faith Gallegos"+      }+    ],+    "greeting": "Hello, Watson Tucker! You have 9 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a90780b195513995278",+    "index": 64,+    "guid": "625810dc-6353-4387-862c-e71ed6115966",+    "isActive": false,+    "balance": "$2,428.28",+    "picture": "http://placehold.it/32x32",+    "age": 33,+    "eyeColor": "green",+    "name": "Levy Ortega",+    "gender": "male",+    "company": "FIBEROX",+    "email": "levyortega@fiberox.com",+    "phone": "+1 (819) 514-2393",+    "address": "485 Dunne Place, Sena, Idaho, 1583",+    "about": "Exercitation reprehenderit voluptate aliqua laborum dolore consequat pariatur ipsum occaecat culpa et dolore voluptate. Laborum ut adipisicing exercitation irure ipsum ea. Occaecat aute officia et qui id elit adipisicing consectetur. Laborum ullamco pariatur eiusmod dolore aliqua ea nostrud. Occaecat ullamco nostrud deserunt ea officia. Deserunt incididunt et laboris veniam laboris adipisicing culpa laborum occaecat culpa sit labore. Irure in ipsum exercitation ea laboris culpa.\r\n",+    "registered": "2014-06-19T01:29:28 +07:00",+    "latitude": -66.489978,+    "longitude": 35.142253,+    "tags": [+      "consequat",+      "ut",+      "ipsum",+      "ullamco",+      "anim",+      "irure",+      "consectetur"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Nellie Webb"+      },+      {+        "id": 1,+        "name": "Sheri Roy"+      },+      {+        "id": 2,+        "name": "Elizabeth Cooke"+      }+    ],+    "greeting": "Hello, Levy Ortega! You have 3 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a901df67d5f687df4ab",+    "index": 65,+    "guid": "73d49a86-1caa-4ef5-b637-aef68b215e96",+    "isActive": true,+    "balance": "$3,990.52",+    "picture": "http://placehold.it/32x32",+    "age": 24,+    "eyeColor": "blue",+    "name": "Webster Conway",+    "gender": "male",+    "company": "PRISMATIC",+    "email": "websterconway@prismatic.com",+    "phone": "+1 (857) 462-3648",+    "address": "557 Dekoven Court, Sandston, Palau, 8537",+    "about": "Fugiat non velit fugiat sint minim magna occaecat deserunt enim adipisicing excepteur amet. Esse incididunt mollit nisi ut id consectetur qui laborum consequat et occaecat. Tempor cupidatat enim sunt cillum. Laborum consequat Lorem enim fugiat id voluptate incididunt aliquip eu esse dolor duis ad. Officia dolore duis nisi veniam ut ut dolore fugiat deserunt aute consectetur. Excepteur voluptate mollit velit duis ullamco voluptate ullamco.\r\n",+    "registered": "2014-04-30T14:39:28 +07:00",+    "latitude": -67.082095,+    "longitude": 38.979656,+    "tags": [+      "amet",+      "aliquip",+      "ea",+      "culpa",+      "incididunt",+      "dolor",+      "fugiat"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Sharp Ramirez"+      },+      {+        "id": 1,+        "name": "Leona Campbell"+      },+      {+        "id": 2,+        "name": "Hyde Reyes"+      }+    ],+    "greeting": "Hello, Webster Conway! You have 3 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a90f7b820f93d4df443",+    "index": 66,+    "guid": "1a306b16-3a38-426c-9254-22dd7264c108",+    "isActive": true,+    "balance": "$2,194.31",+    "picture": "http://placehold.it/32x32",+    "age": 33,+    "eyeColor": "brown",+    "name": "Hill Lancaster",+    "gender": "male",+    "company": "UNIWORLD",+    "email": "hilllancaster@uniworld.com",+    "phone": "+1 (836) 462-3865",+    "address": "786 Dictum Court, Roulette, Minnesota, 3625",+    "about": "Adipisicing ea commodo et nulla ullamco fugiat do magna occaecat consectetur cupidatat nisi fugiat consequat. Velit veniam eu cupidatat adipisicing culpa non non velit amet labore. Dolore dolor enim eiusmod ipsum excepteur non sint ullamco irure laboris nulla id.\r\n",+    "registered": "2014-03-09T12:51:14 +07:00",+    "latitude": -65.924237,+    "longitude": 161.521943,+    "tags": [+      "amet",+      "id",+      "aliquip",+      "aliquip",+      "exercitation",+      "excepteur",+      "cupidatat"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Morrow Mullen"+      },+      {+        "id": 1,+        "name": "Calderon Campos"+      },+      {+        "id": 2,+        "name": "Janis Ramsey"+      }+    ],+    "greeting": "Hello, Hill Lancaster! You have 10 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a906c7f0305536c965c",+    "index": 67,+    "guid": "d1dadd20-7ced-447d-ba72-61e886f4195e",+    "isActive": true,+    "balance": "$3,014.64",+    "picture": "http://placehold.it/32x32",+    "age": 25,+    "eyeColor": "brown",+    "name": "Fletcher Pearson",+    "gender": "male",+    "company": "QOT",+    "email": "fletcherpearson@qot.com",+    "phone": "+1 (930) 439-2213",+    "address": "779 Middagh Street, Crayne, Illinois, 2229",+    "about": "Nulla sit officia velit dolore eiusmod veniam anim laborum Lorem anim et enim irure. Quis labore ad proident occaecat dolore tempor occaecat dolor dolore aliqua laboris esse enim. Dolore tempor cupidatat aute labore aliqua nulla non est.\r\n",+    "registered": "2014-04-25T09:09:10 +07:00",+    "latitude": -2.329892,+    "longitude": 135.411164,+    "tags": [+      "magna",+      "ipsum",+      "tempor",+      "velit",+      "esse",+      "magna",+      "nisi"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Tonya Ashley"+      },+      {+        "id": 1,+        "name": "Latisha Burris"+      },+      {+        "id": 2,+        "name": "Tracey Rowe"+      }+    ],+    "greeting": "Hello, Fletcher Pearson! You have 9 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a9057e5269ab81d8e18",+    "index": 68,+    "guid": "4723bd56-76cb-4a30-bdf5-8b4c4c9805aa",+    "isActive": false,+    "balance": "$3,114.82",+    "picture": "http://placehold.it/32x32",+    "age": 23,+    "eyeColor": "blue",+    "name": "Odessa Lawson",+    "gender": "female",+    "company": "JOVIOLD",+    "email": "odessalawson@joviold.com",+    "phone": "+1 (921) 400-3177",+    "address": "443 Ryder Avenue, Camino, Missouri, 487",+    "about": "Labore aliquip nulla est eiusmod amet Lorem duis fugiat duis do excepteur voluptate in non. Consequat nisi reprehenderit ullamco eu sit id consectetur commodo culpa consequat dolore aliqua. Amet nulla ut in laboris dolor do ad laboris eu commodo qui dolore pariatur. Et qui cillum anim dolore ipsum excepteur id laboris minim dolor.\r\n",+    "registered": "2014-03-26T22:07:14 +07:00",+    "latitude": 18.291259,+    "longitude": -86.22914,+    "tags": [+      "nisi",+      "ut",+      "dolore",+      "ullamco",+      "cupidatat",+      "nulla",+      "laborum"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Vonda Cote"+      },+      {+        "id": 1,+        "name": "Lewis Gonzales"+      },+      {+        "id": 2,+        "name": "Jacqueline Richmond"+      }+    ],+    "greeting": "Hello, Odessa Lawson! You have 4 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a90f748a6195f1c2b42",+    "index": 69,+    "guid": "697c2775-d924-436d-ba9f-e21bd4fa0f73",+    "isActive": false,+    "balance": "$1,269.09",+    "picture": "http://placehold.it/32x32",+    "age": 40,+    "eyeColor": "green",+    "name": "Selena Nieves",+    "gender": "female",+    "company": "INTRAWEAR",+    "email": "selenanieves@intrawear.com",+    "phone": "+1 (862) 476-3196",+    "address": "101 Harkness Avenue, Worton, Connecticut, 400",+    "about": "Sunt aliquip exercitation dolor dolore nostrud anim enim anim est officia. Minim ea Lorem ullamco reprehenderit officia reprehenderit ex ex reprehenderit. Qui nulla ullamco consequat minim exercitation. Veniam id eu pariatur ut veniam dolore aliquip qui aliqua incididunt ut.\r\n",+    "registered": "2014-03-07T15:11:53 +08:00",+    "latitude": -43.588256,+    "longitude": 43.731157,+    "tags": [+      "duis",+      "duis",+      "reprehenderit",+      "ad",+      "magna",+      "excepteur",+      "officia"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Rutledge Hines"+      },+      {+        "id": 1,+        "name": "Carrillo Johnson"+      },+      {+        "id": 2,+        "name": "Morgan Parks"+      }+    ],+    "greeting": "Hello, Selena Nieves! You have 9 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a90628ac7e339a407a5",+    "index": 70,+    "guid": "6ddd302b-fd62-47f9-aa8a-9fb2c35172c5",+    "isActive": true,+    "balance": "$2,185.43",+    "picture": "http://placehold.it/32x32",+    "age": 34,+    "eyeColor": "blue",+    "name": "Georgia Moran",+    "gender": "female",+    "company": "EARTHMARK",+    "email": "georgiamoran@earthmark.com",+    "phone": "+1 (970) 439-3884",+    "address": "848 Fay Court, Hayes, Maine, 7603",+    "about": "Ea do et excepteur dolor cupidatat nisi. Quis deserunt cupidatat duis enim eu deserunt anim enim. Cillum consectetur occaecat exercitation occaecat duis dolor voluptate officia exercitation irure. Occaecat magna proident enim exercitation veniam. Est commodo ullamco minim esse enim commodo ea commodo nulla et voluptate aliqua anim. Quis magna commodo nulla quis sit duis id et ex id ea dolore anim.\r\n",+    "registered": "2014-04-22T10:10:46 +07:00",+    "latitude": -78.673083,+    "longitude": 167.271688,+    "tags": [+      "non",+      "labore",+      "adipisicing",+      "cupidatat",+      "cupidatat",+      "quis",+      "aliquip"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Andrews Love"+      },+      {+        "id": 1,+        "name": "Brianna Yates"+      },+      {+        "id": 2,+        "name": "Jennifer Bass"+      }+    ],+    "greeting": "Hello, Georgia Moran! You have 10 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a901ec4ee748cbbc125",+    "index": 71,+    "guid": "0e5fc95f-e605-4baa-bc36-b2f795c42e03",+    "isActive": false,+    "balance": "$2,256.63",+    "picture": "http://placehold.it/32x32",+    "age": 22,+    "eyeColor": "blue",+    "name": "House Sargent",+    "gender": "male",+    "company": "FANFARE",+    "email": "housesargent@fanfare.com",+    "phone": "+1 (877) 416-3590",+    "address": "961 Olive Street, Sultana, Puerto Rico, 3764",+    "about": "Cillum minim aliquip sit nulla aute commodo pariatur reprehenderit aliqua laboris in sint velit. Qui irure ut nostrud elit aliqua reprehenderit tempor quis laboris. Voluptate enim eiusmod laborum est excepteur enim est. Veniam non magna ea proident deserunt ipsum Lorem adipisicing velit eiusmod. Culpa id ea non ex ad.\r\n",+    "registered": "2014-04-27T09:04:21 +07:00",+    "latitude": -54.902834,+    "longitude": 147.773359,+    "tags": [+      "cupidatat",+      "voluptate",+      "dolor",+      "excepteur",+      "exercitation",+      "id",+      "in"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Francis Alford"+      },+      {+        "id": 1,+        "name": "Berta Sparks"+      },+      {+        "id": 2,+        "name": "Laura Hahn"+      }+    ],+    "greeting": "Hello, House Sargent! You have 9 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a90b51db1c7d287288e",+    "index": 72,+    "guid": "574450df-adad-4071-b642-0971f5f7928e",+    "isActive": true,+    "balance": "$2,263.30",+    "picture": "http://placehold.it/32x32",+    "age": 38,+    "eyeColor": "green",+    "name": "Roseann Lambert",+    "gender": "female",+    "company": "ROUGHIES",+    "email": "roseannlambert@roughies.com",+    "phone": "+1 (908) 551-2196",+    "address": "142 Girard Street, Levant, New Mexico, 2920",+    "about": "Aliquip ea elit anim aliqua in ea. Ex nisi nulla veniam amet. Occaecat pariatur qui consectetur consectetur anim ut anim cillum consectetur adipisicing pariatur. Cillum quis aliquip ad do in fugiat minim et ad labore sit ut aliqua cillum. Elit enim laboris incididunt laboris. Est in ullamco laboris deserunt aliqua veniam. Aute cillum culpa do dolore.\r\n",+    "registered": "2014-04-29T13:57:00 +07:00",+    "latitude": 0.812689,+    "longitude": 127.517728,+    "tags": [+      "veniam",+      "quis",+      "do",+      "aliqua",+      "deserunt",+      "id",+      "ut"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Amelia Dunlap"+      },+      {+        "id": 1,+        "name": "Benson Huff"+      },+      {+        "id": 2,+        "name": "Aimee Fisher"+      }+    ],+    "greeting": "Hello, Roseann Lambert! You have 6 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a90285dfadf5a8ab1db",+    "index": 73,+    "guid": "cf2c0b72-e90e-43d1-a5ee-ff85e28272ec",+    "isActive": false,+    "balance": "$3,053.88",+    "picture": "http://placehold.it/32x32",+    "age": 20,+    "eyeColor": "brown",+    "name": "Althea Burgess",+    "gender": "female",+    "company": "RODEMCO",+    "email": "altheaburgess@rodemco.com",+    "phone": "+1 (946) 406-2326",+    "address": "127 Fleet Place, Northridge, Northern Mariana Islands, 2106",+    "about": "Ea aliquip mollit consectetur non sunt. Deserunt adipisicing sint non excepteur veniam. Sunt eu ad Lorem qui laborum labore. Dolor ut esse veniam dolor officia. Minim occaecat labore voluptate exercitation fugiat eu labore non dolor ipsum do magna qui incididunt.\r\n",+    "registered": "2014-03-16T23:19:14 +07:00",+    "latitude": -63.483213,+    "longitude": 118.593144,+    "tags": [+      "amet",+      "veniam",+      "consequat",+      "deserunt",+      "magna",+      "ea",+      "aliqua"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Delacruz Pollard"+      },+      {+        "id": 1,+        "name": "Kelly Washington"+      },+      {+        "id": 2,+        "name": "Irma Freeman"+      }+    ],+    "greeting": "Hello, Althea Burgess! You have 10 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a902435e7e0219ce954",+    "index": 74,+    "guid": "e3079855-52fe-494a-bb64-cf88934d1f84",+    "isActive": false,+    "balance": "$1,615.23",+    "picture": "http://placehold.it/32x32",+    "age": 28,+    "eyeColor": "brown",+    "name": "Ursula French",+    "gender": "female",+    "company": "KLUGGER",+    "email": "ursulafrench@klugger.com",+    "phone": "+1 (803) 454-3759",+    "address": "768 Bancroft Place, Layhill, Mississippi, 3591",+    "about": "Anim proident aliquip non amet. Dolore occaecat non irure cillum excepteur eu officia irure ea enim incididunt. Adipisicing ad esse voluptate duis dolor irure. Non sunt cillum est exercitation labore cillum minim consequat occaecat est aliqua. Ex adipisicing consequat aliqua pariatur est elit deserunt. Proident fugiat aliquip culpa magna ut deserunt incididunt eu enim esse fugiat eu consequat amet. Ad sit do Lorem aliquip enim ea magna.\r\n",+    "registered": "2014-08-01T00:12:36 +07:00",+    "latitude": -31.044528,+    "longitude": -131.432956,+    "tags": [+      "consequat",+      "enim",+      "nisi",+      "cupidatat",+      "est",+      "sint",+      "nostrud"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Mai Hudson"+      },+      {+        "id": 1,+        "name": "Erickson Perkins"+      },+      {+        "id": 2,+        "name": "Cleveland Armstrong"+      }+    ],+    "greeting": "Hello, Ursula French! You have 8 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a906ae58205e4d302e3",+    "index": 75,+    "guid": "f8b68baa-b922-4a5f-9d4a-fa7d9eee543c",+    "isActive": true,+    "balance": "$1,500.61",+    "picture": "http://placehold.it/32x32",+    "age": 29,+    "eyeColor": "blue",+    "name": "Lee Kelley",+    "gender": "male",+    "company": "INQUALA",+    "email": "leekelley@inquala.com",+    "phone": "+1 (947) 551-3499",+    "address": "973 Bayard Street, Orovada, North Dakota, 8499",+    "about": "Ullamco labore fugiat in minim duis duis reprehenderit deserunt eiusmod nostrud cillum laborum. Incididunt occaecat duis esse consectetur voluptate velit pariatur. Pariatur et qui sint anim sunt fugiat ut proident nulla fugiat. Nostrud ea sunt occaecat nulla qui id cupidatat proident ut.\r\n",+    "registered": "2014-11-01T01:18:21 +07:00",+    "latitude": -59.393379,+    "longitude": -110.276914,+    "tags": [+      "sit",+      "magna",+      "in",+      "consequat",+      "excepteur",+      "consequat",+      "Lorem"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Walton Daniels"+      },+      {+        "id": 1,+        "name": "Hammond Robbins"+      },+      {+        "id": 2,+        "name": "Evangelina Chaney"+      }+    ],+    "greeting": "Hello, Lee Kelley! You have 8 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a902a956e2130c70b9c",+    "index": 76,+    "guid": "3f62418d-23f0-4ed2-a76f-51645437223b",+    "isActive": false,+    "balance": "$2,477.19",+    "picture": "http://placehold.it/32x32",+    "age": 28,+    "eyeColor": "green",+    "name": "Miles Poole",+    "gender": "male",+    "company": "YOGASM",+    "email": "milespoole@yogasm.com",+    "phone": "+1 (866) 412-2294",+    "address": "744 Sutton Street, Richford, Vermont, 7668",+    "about": "Nostrud reprehenderit officia officia ex proident sint nulla sint quis duis laborum duis. Anim adipisicing officia officia anim aliquip incididunt culpa duis sint do ullamco ex consectetur. Commodo et tempor eu consequat laboris tempor ipsum magna elit labore consectetur excepteur.\r\n",+    "registered": "2014-07-20T11:37:28 +07:00",+    "latitude": 23.618997,+    "longitude": -80.142779,+    "tags": [+      "magna",+      "exercitation",+      "Lorem",+      "nostrud",+      "pariatur",+      "ut",+      "laborum"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Claudine Gill"+      },+      {+        "id": 1,+        "name": "Huff Kramer"+      },+      {+        "id": 2,+        "name": "Bryan Doyle"+      }+    ],+    "greeting": "Hello, Miles Poole! You have 10 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a90357b2420c0dc0895",+    "index": 77,+    "guid": "ef7aa63d-53b9-4e85-ac36-4cfb1afd214a",+    "isActive": true,+    "balance": "$1,105.70",+    "picture": "http://placehold.it/32x32",+    "age": 39,+    "eyeColor": "green",+    "name": "Cohen Clemons",+    "gender": "male",+    "company": "EARBANG",+    "email": "cohenclemons@earbang.com",+    "phone": "+1 (936) 599-3992",+    "address": "952 Paerdegat Avenue, Frizzleburg, Rhode Island, 7905",+    "about": "Consequat exercitation fugiat ut labore. Ad magna sunt dolore magna nulla. Laborum deserunt magna eu incididunt magna occaecat ut sit aliqua nisi culpa dolor ullamco esse. Enim adipisicing proident proident enim qui est ad non. Ex proident sit irure cillum in nostrud veniam amet excepteur voluptate commodo eu eiusmod ullamco.\r\n",+    "registered": "2014-06-15T00:23:03 +07:00",+    "latitude": 78.28454,+    "longitude": 64.170517,+    "tags": [+      "non",+      "ex",+      "sit",+      "ipsum",+      "esse",+      "enim",+      "minim"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Sanchez Wolf"+      },+      {+        "id": 1,+        "name": "Trudy Melendez"+      },+      {+        "id": 2,+        "name": "Magdalena Hatfield"+      }+    ],+    "greeting": "Hello, Cohen Clemons! You have 3 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a90ae5beac7dec2ba78",+    "index": 78,+    "guid": "1bccbb43-2801-4c99-a646-001622f3653c",+    "isActive": false,+    "balance": "$1,246.07",+    "picture": "http://placehold.it/32x32",+    "age": 35,+    "eyeColor": "blue",+    "name": "Evelyn Combs",+    "gender": "female",+    "company": "BITENDREX",+    "email": "evelyncombs@bitendrex.com",+    "phone": "+1 (937) 463-3648",+    "address": "498 Beayer Place, Caln, Michigan, 637",+    "about": "Laboris ipsum ea est nisi exercitation adipisicing commodo ea ipsum tempor. Laboris duis sit aliquip reprehenderit id cillum officia. Tempor velit reprehenderit adipisicing dolor ut qui id magna consequat. Fugiat proident sunt sit reprehenderit duis. Eu ipsum adipisicing eu magna cupidatat mollit esse.\r\n",+    "registered": "2014-06-14T20:28:41 +07:00",+    "latitude": 13.771679,+    "longitude": 58.844141,+    "tags": [+      "ex",+      "irure",+      "quis",+      "velit",+      "deserunt",+      "mollit",+      "irure"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Melton Wright"+      },+      {+        "id": 1,+        "name": "Letitia Osborn"+      },+      {+        "id": 2,+        "name": "Delgado Hebert"+      }+    ],+    "greeting": "Hello, Evelyn Combs! You have 7 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a90a58e6a9e15665417",+    "index": 79,+    "guid": "6c039c25-e902-4121-9319-aa5e25f09b90",+    "isActive": true,+    "balance": "$1,839.17",+    "picture": "http://placehold.it/32x32",+    "age": 27,+    "eyeColor": "blue",+    "name": "Lakeisha Glover",+    "gender": "female",+    "company": "REALMO",+    "email": "lakeishaglover@realmo.com",+    "phone": "+1 (962) 444-3749",+    "address": "259 Homecrest Court, Villarreal, Oklahoma, 5463",+    "about": "Lorem cupidatat non elit mollit commodo tempor dolore sint qui officia fugiat mollit ex. Ea commodo minim labore commodo commodo sint occaecat eu aute est excepteur labore eiusmod ut. Fugiat voluptate laborum mollit aliqua ea. Ut fugiat culpa do velit non nisi voluptate exercitation. Duis elit velit adipisicing esse.\r\n",+    "registered": "2014-10-07T01:42:52 +07:00",+    "latitude": 7.777536,+    "longitude": -148.148716,+    "tags": [+      "officia",+      "Lorem",+      "eu",+      "eiusmod",+      "mollit",+      "duis",+      "exercitation"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Hodge Snider"+      },+      {+        "id": 1,+        "name": "Maritza Gilliam"+      },+      {+        "id": 2,+        "name": "Barbra Harding"+      }+    ],+    "greeting": "Hello, Lakeisha Glover! You have 5 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a904e60ffa71e64c330",+    "index": 80,+    "guid": "764804ce-2ded-4ab7-aef7-cc638d74a1f6",+    "isActive": true,+    "balance": "$3,096.79",+    "picture": "http://placehold.it/32x32",+    "age": 36,+    "eyeColor": "green",+    "name": "Hall Woodard",+    "gender": "male",+    "company": "HOTCAKES",+    "email": "hallwoodard@hotcakes.com",+    "phone": "+1 (923) 579-2251",+    "address": "678 Johnson Avenue, Nescatunga, West Virginia, 5718",+    "about": "Nulla Lorem proident incididunt nulla amet. Consectetur ea fugiat est adipisicing nisi exercitation ea irure. Id nisi anim culpa incididunt ea laborum. Non est aute anim deserunt ad cupidatat.\r\n",+    "registered": "2014-07-25T16:17:12 +07:00",+    "latitude": -72.300952,+    "longitude": 149.908938,+    "tags": [+      "magna",+      "fugiat",+      "laboris",+      "nulla",+      "tempor",+      "consequat",+      "officia"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Tessa Mccray"+      },+      {+        "id": 1,+        "name": "Hendricks Larson"+      },+      {+        "id": 2,+        "name": "Cecilia Mooney"+      }+    ],+    "greeting": "Hello, Hall Woodard! You have 1 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a9095c0ba3039bba2a0",+    "index": 81,+    "guid": "477bf131-1833-4ceb-a02d-367662e8a0ad",+    "isActive": true,+    "balance": "$3,975.04",+    "picture": "http://placehold.it/32x32",+    "age": 20,+    "eyeColor": "brown",+    "name": "Mccray Holman",+    "gender": "male",+    "company": "NETROPIC",+    "email": "mccrayholman@netropic.com",+    "phone": "+1 (888) 582-2650",+    "address": "930 Alton Place, Stouchsburg, Iowa, 8307",+    "about": "Officia laboris laboris ea magna nulla exercitation ullamco est magna proident cupidatat dolor. Et dolore fugiat nostrud elit in Lorem cillum eu incididunt ullamco irure eiusmod voluptate aliqua. Sint velit veniam nisi in amet incididunt ex et voluptate. Amet laborum sint fugiat nulla dolor adipisicing eiusmod ea. Labore amet nisi et pariatur duis non aliquip ea pariatur sit. Est eu dolore consequat ex quis qui adipisicing minim ea. Proident eiusmod enim irure cillum eu do laborum do voluptate occaecat id consectetur irure dolor.\r\n",+    "registered": "2014-07-15T14:32:20 +07:00",+    "latitude": 69.28999,+    "longitude": 20.24303,+    "tags": [+      "deserunt",+      "labore",+      "voluptate",+      "sint",+      "occaecat",+      "non",+      "sunt"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Ashley Mcintosh"+      },+      {+        "id": 1,+        "name": "Augusta Barr"+      },+      {+        "id": 2,+        "name": "Page Wiley"+      }+    ],+    "greeting": "Hello, Mccray Holman! You have 8 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a908bfd72d2f3594530",+    "index": 82,+    "guid": "e7128ea4-81e3-4888-ba11-4f3670e473ea",+    "isActive": false,+    "balance": "$3,306.06",+    "picture": "http://placehold.it/32x32",+    "age": 22,+    "eyeColor": "green",+    "name": "Sherri Villarreal",+    "gender": "female",+    "company": "TERRAGEN",+    "email": "sherrivillarreal@terragen.com",+    "phone": "+1 (881) 513-2832",+    "address": "922 Surf Avenue, Oberlin, Federated States Of Micronesia, 8301",+    "about": "Occaecat Lorem nostrud sint esse ullamco voluptate cillum sit aliquip sint nostrud. Velit aliqua cupidatat eu adipisicing sunt aute dolore cillum. Voluptate laboris exercitation magna non elit. Excepteur eiusmod enim amet fugiat quis. Anim cillum aute sit magna fugiat ut aliquip incididunt enim.\r\n",+    "registered": "2014-01-29T03:48:07 +08:00",+    "latitude": 70.685812,+    "longitude": -159.885272,+    "tags": [+      "adipisicing",+      "officia",+      "laborum",+      "ipsum",+      "voluptate",+      "sint",+      "cupidatat"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Vinson Berg"+      },+      {+        "id": 1,+        "name": "Emily Strong"+      },+      {+        "id": 2,+        "name": "Lori Gonzalez"+      }+    ],+    "greeting": "Hello, Sherri Villarreal! You have 1 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a90ed7615708c5d1f8e",+    "index": 83,+    "guid": "cb7d9a4c-9739-43c8-b75e-0d3cdf462e89",+    "isActive": false,+    "balance": "$3,087.41",+    "picture": "http://placehold.it/32x32",+    "age": 28,+    "eyeColor": "brown",+    "name": "Little Nielsen",+    "gender": "male",+    "company": "LUDAK",+    "email": "littlenielsen@ludak.com",+    "phone": "+1 (826) 557-2684",+    "address": "842 Banner Avenue, Gadsden, Kentucky, 3196",+    "about": "Officia nostrud anim aliqua veniam ut. Non culpa et dolore nostrud elit esse sint mollit anim pariatur id velit irure culpa. Officia irure adipisicing velit velit culpa sunt proident occaecat irure voluptate ullamco. Labore mollit pariatur minim in cillum deserunt. Sint irure ex laborum et minim id ea fugiat excepteur velit veniam velit consequat sint. Culpa anim velit elit officia irure exercitation ex labore est amet. Do do eiusmod ipsum officia veniam laboris irure sint eiusmod non cillum eiusmod.\r\n",+    "registered": "2014-09-19T15:02:29 +07:00",+    "latitude": 26.743234,+    "longitude": -53.174043,+    "tags": [+      "et",+      "sint",+      "culpa",+      "dolore",+      "ea",+      "irure",+      "excepteur"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Gillespie Branch"+      },+      {+        "id": 1,+        "name": "Cantu Kent"+      },+      {+        "id": 2,+        "name": "Nell Meyer"+      }+    ],+    "greeting": "Hello, Little Nielsen! You have 5 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a909137471ca6b126c8",+    "index": 84,+    "guid": "169b0cbf-781c-4b46-8285-e18b3aa6bc0e",+    "isActive": true,+    "balance": "$2,853.96",+    "picture": "http://placehold.it/32x32",+    "age": 33,+    "eyeColor": "green",+    "name": "Kristi Marquez",+    "gender": "female",+    "company": "RONELON",+    "email": "kristimarquez@ronelon.com",+    "phone": "+1 (838) 414-3765",+    "address": "566 Love Lane, Fresno, New Jersey, 5219",+    "about": "Proident enim commodo ut nisi minim esse eiusmod nisi irure consequat proident voluptate. Culpa ut sunt incididunt sint minim duis excepteur nulla cupidatat ullamco proident nisi ex enim. Qui culpa ex velit do pariatur Lorem dolor ad labore adipisicing magna sunt voluptate dolore. Lorem minim commodo non amet. Nulla reprehenderit consectetur ad nisi voluptate. Ea consectetur deserunt dolore ut. Esse duis consequat reprehenderit cillum commodo consequat ex consectetur do sint velit non deserunt.\r\n",+    "registered": "2014-02-21T07:19:48 +08:00",+    "latitude": 38.750345,+    "longitude": -111.349096,+    "tags": [+      "minim",+      "minim",+      "occaecat",+      "tempor",+      "dolore",+      "fugiat",+      "laborum"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Eddie Soto"+      },+      {+        "id": 1,+        "name": "Valencia Serrano"+      },+      {+        "id": 2,+        "name": "Sears Richards"+      }+    ],+    "greeting": "Hello, Kristi Marquez! You have 3 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a90deb8bb21d9c88b7a",+    "index": 85,+    "guid": "58c6180e-8bc6-4ae0-9988-52df23beedac",+    "isActive": false,+    "balance": "$2,028.59",+    "picture": "http://placehold.it/32x32",+    "age": 39,+    "eyeColor": "blue",+    "name": "Callie Sweeney",+    "gender": "female",+    "company": "POLARIA",+    "email": "calliesweeney@polaria.com",+    "phone": "+1 (999) 576-3446",+    "address": "278 Gunnison Court, Wakulla, Nevada, 3605",+    "about": "Minim non eu sint culpa. Non irure ea eiusmod est dolore id culpa laborum ex aliquip et eu. Laborum occaecat labore incididunt et ipsum adipisicing cupidatat eu consectetur reprehenderit quis dolor.\r\n",+    "registered": "2014-11-06T17:55:38 +08:00",+    "latitude": 22.578553,+    "longitude": 70.713617,+    "tags": [+      "veniam",+      "ad",+      "aliqua",+      "id",+      "quis",+      "esse",+      "est"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Cochran Raymond"+      },+      {+        "id": 1,+        "name": "Perry Pugh"+      },+      {+        "id": 2,+        "name": "Walsh Cummings"+      }+    ],+    "greeting": "Hello, Callie Sweeney! You have 8 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a9092b4caeee42e4083",+    "index": 86,+    "guid": "b37a99a1-89ea-4257-bd5c-f8070c6e5d72",+    "isActive": false,+    "balance": "$2,333.35",+    "picture": "http://placehold.it/32x32",+    "age": 27,+    "eyeColor": "brown",+    "name": "Henson Parrish",+    "gender": "male",+    "company": "MANTRO",+    "email": "hensonparrish@mantro.com",+    "phone": "+1 (983) 409-3702",+    "address": "749 Malbone Street, Century, California, 6571",+    "about": "Pariatur nisi magna laboris ipsum ad sint. Exercitation sit id reprehenderit fugiat ea dolor. Duis exercitation voluptate nulla duis enim culpa eiusmod nulla mollit et occaecat eu Lorem.\r\n",+    "registered": "2014-06-21T02:15:00 +07:00",+    "latitude": 42.27087,+    "longitude": 88.230275,+    "tags": [+      "sunt",+      "excepteur",+      "veniam",+      "cillum",+      "sunt",+      "consequat",+      "ullamco"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Dunn Cantrell"+      },+      {+        "id": 1,+        "name": "Mendoza Keller"+      },+      {+        "id": 2,+        "name": "Josefa Green"+      }+    ],+    "greeting": "Hello, Henson Parrish! You have 7 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a9001009b488b9deadf",+    "index": 87,+    "guid": "4a8e033a-6160-44a0-b6f6-e07f11455db9",+    "isActive": false,+    "balance": "$2,886.19",+    "picture": "http://placehold.it/32x32",+    "age": 33,+    "eyeColor": "blue",+    "name": "Mcgowan Hogan",+    "gender": "male",+    "company": "MAGNEMO",+    "email": "mcgowanhogan@magnemo.com",+    "phone": "+1 (816) 507-2353",+    "address": "979 Reed Street, Roberts, Maryland, 9773",+    "about": "Sunt sint labore aliqua nisi ipsum mollit. Lorem aliquip nisi Lorem eu velit. Laborum reprehenderit minim cillum culpa cupidatat commodo do aute dolore. Sunt ullamco consectetur ex officia anim fugiat quis fugiat laboris est quis officia aliqua pariatur. Adipisicing nulla culpa officia ut reprehenderit ea cupidatat.\r\n",+    "registered": "2014-05-26T12:57:18 +07:00",+    "latitude": -24.348796,+    "longitude": 25.886101,+    "tags": [+      "ex",+      "ullamco",+      "non",+      "ea",+      "enim",+      "quis",+      "ipsum"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Terry Burton"+      },+      {+        "id": 1,+        "name": "Cara Estes"+      },+      {+        "id": 2,+        "name": "Johanna Dillard"+      }+    ],+    "greeting": "Hello, Mcgowan Hogan! You have 1 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a9021cae47006152c91",+    "index": 88,+    "guid": "ebae8f41-1ba7-4f91-8d61-1a1a0d6b515f",+    "isActive": false,+    "balance": "$2,168.49",+    "picture": "http://placehold.it/32x32",+    "age": 25,+    "eyeColor": "brown",+    "name": "Riddle Alvarado",+    "gender": "male",+    "company": "INTERLOO",+    "email": "riddlealvarado@interloo.com",+    "phone": "+1 (892) 530-2012",+    "address": "488 Loring Avenue, Homeworth, Washington, 8337",+    "about": "Officia labore commodo eu consectetur. Consectetur consequat voluptate minim dolor do anim magna deserunt ullamco. Mollit laborum eiusmod veniam velit officia dolore dolore in ex veniam officia eu elit fugiat. Labore non in Lorem nulla irure ea id cupidatat eu consectetur. Tempor non quis eu nulla aliquip proident elit commodo duis laboris laborum eu amet occaecat.\r\n",+    "registered": "2014-11-13T11:12:08 +08:00",+    "latitude": 21.377099,+    "longitude": 128.211659,+    "tags": [+      "ex",+      "excepteur",+      "magna",+      "enim",+      "magna",+      "et",+      "incididunt"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Elisa Vinson"+      },+      {+        "id": 1,+        "name": "Peterson Reynolds"+      },+      {+        "id": 2,+        "name": "Mason Odonnell"+      }+    ],+    "greeting": "Hello, Riddle Alvarado! You have 1 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a901459251e4d519953",+    "index": 89,+    "guid": "6fe0652a-7055-47d0-8b6a-8bc3b3f6a7d0",+    "isActive": true,+    "balance": "$1,638.04",+    "picture": "http://placehold.it/32x32",+    "age": 29,+    "eyeColor": "blue",+    "name": "Stone Maynard",+    "gender": "male",+    "company": "CONJURICA",+    "email": "stonemaynard@conjurica.com",+    "phone": "+1 (827) 532-2021",+    "address": "631 Orient Avenue, Homestead, Utah, 8151",+    "about": "In voluptate voluptate veniam reprehenderit dolor est ut est voluptate dolore occaecat voluptate. Laboris cillum ex aliquip quis dolor qui qui fugiat sit. Veniam magna dolor mollit sint laborum duis id duis aliqua dolore reprehenderit qui et. Irure cupidatat ea sunt anim amet mollit adipisicing id sint deserunt veniam irure. Reprehenderit elit mollit do ex reprehenderit ipsum aliqua qui. Ea non eiusmod officia laboris dolor tempor deserunt elit velit ex aliqua. Duis reprehenderit duis duis elit irure elit dolore dolore.\r\n",+    "registered": "2014-05-26T12:55:16 +07:00",+    "latitude": -20.476755,+    "longitude": 76.876321,+    "tags": [+      "fugiat",+      "dolore",+      "consequat",+      "do",+      "esse",+      "consectetur",+      "anim"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Mckenzie Bartlett"+      },+      {+        "id": 1,+        "name": "Villarreal Welch"+      },+      {+        "id": 2,+        "name": "Amparo Kelly"+      }+    ],+    "greeting": "Hello, Stone Maynard! You have 5 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a9015625be6710ded3e",+    "index": 90,+    "guid": "d079e795-b71e-446b-91f8-6347ca0e2e57",+    "isActive": false,+    "balance": "$1,156.54",+    "picture": "http://placehold.it/32x32",+    "age": 28,+    "eyeColor": "blue",+    "name": "Baird Jones",+    "gender": "male",+    "company": "KNEEDLES",+    "email": "bairdjones@kneedles.com",+    "phone": "+1 (898) 574-3748",+    "address": "890 Lancaster Avenue, Falconaire, Wisconsin, 9406",+    "about": "Officia veniam sunt minim ex tempor labore consequat laborum adipisicing officia. Incididunt enim excepteur anim et do cupidatat magna excepteur culpa. Dolore enim ea excepteur non commodo aliqua nostrud commodo exercitation dolor duis. Consectetur consequat ullamco in esse fugiat pariatur anim mollit aliqua aliquip qui. Voluptate cupidatat ipsum labore aliquip fugiat cillum amet sunt voluptate occaecat occaecat nostrud.\r\n",+    "registered": "2014-01-22T19:38:09 +08:00",+    "latitude": -30.096555,+    "longitude": -119.933566,+    "tags": [+      "occaecat",+      "qui",+      "enim",+      "in",+      "pariatur",+      "irure",+      "officia"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Gail Flynn"+      },+      {+        "id": 1,+        "name": "Sanford Harmon"+      },+      {+        "id": 2,+        "name": "Noble Chambers"+      }+    ],+    "greeting": "Hello, Baird Jones! You have 3 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a90b147f5343b9ce16b",+    "index": 91,+    "guid": "af036dfd-72dd-451e-b93d-6dceea4796c4",+    "isActive": false,+    "balance": "$3,703.12",+    "picture": "http://placehold.it/32x32",+    "age": 33,+    "eyeColor": "blue",+    "name": "Leila Gay",+    "gender": "female",+    "company": "ZOXY",+    "email": "leilagay@zoxy.com",+    "phone": "+1 (845) 467-2535",+    "address": "800 Little Street, Troy, Louisiana, 233",+    "about": "Ea aliqua excepteur duis est velit eiusmod irure est eiusmod non elit. Consequat nostrud elit ullamco minim labore deserunt commodo. Mollit amet duis laboris mollit est ex aliquip. Minim et veniam reprehenderit voluptate aliqua id officia excepteur consequat fugiat non ex eu. Fugiat officia cupidatat pariatur voluptate excepteur in.\r\n",+    "registered": "2014-08-02T18:50:58 +07:00",+    "latitude": 59.481938,+    "longitude": 141.038749,+    "tags": [+      "ipsum",+      "commodo",+      "velit",+      "sit",+      "aliquip",+      "ullamco",+      "nisi"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Conway Summers"+      },+      {+        "id": 1,+        "name": "Deirdre Moody"+      },+      {+        "id": 2,+        "name": "Curtis Dickerson"+      }+    ],+    "greeting": "Hello, Leila Gay! You have 10 unread messages.",+    "favoriteFruit": "apple"+  },+  {+    "_id": "54727a90ffed9e529a6fa6b2",+    "index": 92,+    "guid": "7deb0cd2-5374-40c4-a38d-060ef674b2df",+    "isActive": true,+    "balance": "$2,980.94",+    "picture": "http://placehold.it/32x32",+    "age": 20,+    "eyeColor": "brown",+    "name": "Alma England",+    "gender": "female",+    "company": "SOLAREN",+    "email": "almaengland@solaren.com",+    "phone": "+1 (941) 595-3246",+    "address": "727 Covert Street, Nutrioso, Florida, 9152",+    "about": "Commodo quis occaecat voluptate tempor commodo non ipsum quis tempor pariatur elit. Ea fugiat velit dolor ea et occaecat eu ad aute ipsum. In sunt ad reprehenderit duis. Qui deserunt voluptate proident eu officia laborum pariatur incididunt id nisi. Eiusmod adipisicing do pariatur et incididunt aliquip consectetur.\r\n",+    "registered": "2014-08-08T20:51:29 +07:00",+    "latitude": 70.990007,+    "longitude": -119.193428,+    "tags": [+      "irure",+      "cillum",+      "incididunt",+      "nisi",+      "magna",+      "ullamco",+      "ex"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Downs Keith"+      },+      {+        "id": 1,+        "name": "Dawn Velazquez"+      },+      {+        "id": 2,+        "name": "Deanna Simpson"+      }+    ],+    "greeting": "Hello, Alma England! You have 9 unread messages.",+    "favoriteFruit": "strawberry"+  },+  {+    "_id": "54727a9075e4734b622a166a",+    "index": 93,+    "guid": "c0adc5f1-039a-4b52-a8cf-0196f3c23129",+    "isActive": true,+    "balance": "$2,658.69",+    "picture": "http://placehold.it/32x32",+    "age": 21,+    "eyeColor": "brown",+    "name": "Lou Rose",+    "gender": "female",+    "company": "INFOTRIPS",+    "email": "lourose@infotrips.com",+    "phone": "+1 (845) 564-3943",+    "address": "343 Krier Place, Evergreen, Indiana, 5249",+    "about": "Tempor ullamco id reprehenderit sunt veniam incididunt laboris. Ea ut officia do velit dolor officia Lorem consectetur nisi velit nisi. Aliquip dolore minim sint deserunt deserunt est.\r\n",+    "registered": "2014-04-22T07:46:26 +07:00",+    "latitude": 12.216368,+    "longitude": 8.875707,+    "tags": [+      "velit",+      "consectetur",+      "Lorem",+      "est",+      "adipisicing",+      "ad",+      "ullamco"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Coleen Foley"+      },+      {+        "id": 1,+        "name": "Boone Fitzpatrick"+      },+      {+        "id": 2,+        "name": "Elise Osborne"+      }+    ],+    "greeting": "Hello, Lou Rose! You have 5 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a90512b4f65287789e9",+    "index": 94,+    "guid": "26d470db-29c3-4c14-894b-5654462385a5",+    "isActive": false,+    "balance": "$2,751.47",+    "picture": "http://placehold.it/32x32",+    "age": 23,+    "eyeColor": "blue",+    "name": "Velez Wong",+    "gender": "male",+    "company": "FRANSCENE",+    "email": "velezwong@franscene.com",+    "phone": "+1 (821) 577-3492",+    "address": "197 Huron Street, Hilltop, New Hampshire, 6969",+    "about": "Minim id ullamco ullamco do proident Lorem commodo tempor ea. Est ut id non proident eiusmod. Deserunt fugiat enim culpa veniam sint.\r\n",+    "registered": "2014-07-21T05:23:01 +07:00",+    "latitude": -37.262537,+    "longitude": 132.661281,+    "tags": [+      "culpa",+      "non",+      "elit",+      "nulla",+      "laboris",+      "elit",+      "ex"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Charmaine Curry"+      },+      {+        "id": 1,+        "name": "Clara Randall"+      },+      {+        "id": 2,+        "name": "Lauren Jacobs"+      }+    ],+    "greeting": "Hello, Velez Wong! You have 4 unread messages.",+    "favoriteFruit": "banana"+  },+  {+    "_id": "54727a9095dc4743a5a84bd7",+    "index": 95,+    "guid": "1d1b9166-affc-4d15-ab60-b2edc51f29b4",+    "isActive": true,+    "balance": "$1,063.39",+    "picture": "http://placehold.it/32x32",+    "age": 22,+    "eyeColor": "brown",+    "name": "Catalina Baxter",+    "gender": "female",+    "company": "GAZAK",+    "email": "catalinabaxter@gazak.com",+    "phone": "+1 (839) 435-3437",+    "address": "904 Miller Avenue, Callaghan, Kansas, 7896",+    "about": "Ut ex exercitation commodo sint sint ipsum ea incididunt. Sunt nostrud tempor sint occaecat qui laborum culpa ipsum ullamco veniam est excepteur nostrud. Consequat occaecat id adipisicing esse culpa occaecat. Proident veniam laboris aute minim dolore eiusmod nulla minim elit sint.\r\n",+    "registered": "2014-05-02T10:13:41 +07:00",+    "latitude": 18.545597,+    "longitude": -99.991313,+    "tags": [+      "do",+      "proident",+      "reprehenderit",+      "minim",+      "cillum",+      "quis",+      "esse"+    ],+    "friends": [+      {+        "id": 0,+        "name": "Monique Wilcox"+      },+      {+        "id": 1,+        "name": "Hilda Craig"+      },+      {+        "id": 2,+        "name": "Leola Higgins"+      }+    ],+    "greeting": "Hello, Catalina Baxter! You have 10 unread messages.",+    "favoriteFruit": "apple"+  }+]
+ test/json-data/dates-fract.json view
@@ -0,0 +1,1 @@+["2015-02-02T09:10:11.123Z","2015-02-03T09:10:11.000+0000","2014-01-02T09:10:11.333Z","2014-01-03T02:09:12.000-02:00","2013-05-06T07:08:09.444Z","2015-02-02T09:10:11.66+03:00","2015-02-03T09:10:11.66Z","2014-01-02T09:10:11.66-1200","2014-01-03T02:09:12.66Z","2013-05-06T07:08:09.66+00:00"]
+ test/json-data/dates.json view
@@ -0,0 +1,1 @@+["2015-02-02T09:10:11Z","2015-02-03T09:10:11+0000","2014-01-02T09:10:11Z","2014-01-03T02:09:12-0300","2013-05-06T07:08:09Z","2015-02-02T09:10:11+04:00","2015-02-03T09:10:11Z","2014-01-02T09:10:11-11:45","2014-01-03T02:09:12Z","2013-05-06T07:08:09+0000"]
+ test/json-data/example.json view
@@ -0,0 +1,88 @@+{"web-app": {+  "servlet": [   +    {+      "servlet-name": "cofaxCDS",+      "servlet-class": "org.cofax.cds.CDSServlet",+      "init-param": {+        "configGlossary:installationAt": "Philadelphia, PA",+        "configGlossary:adminEmail": "ksm@pobox.com",+        "configGlossary:poweredBy": "Cofax",+        "configGlossary:poweredByIcon": "/images/cofax.gif",+        "configGlossary:staticPath": "/content/static",+        "templateProcessorClass": "org.cofax.WysiwygTemplate",+        "templateLoaderClass": "org.cofax.FilesTemplateLoader",+        "templatePath": "templates",+        "templateOverridePath": "",+        "defaultListTemplate": "listTemplate.htm",+        "defaultFileTemplate": "articleTemplate.htm",+        "useJSP": false,+        "jspListTemplate": "listTemplate.jsp",+        "jspFileTemplate": "articleTemplate.jsp",+        "cachePackageTagsTrack": 200,+        "cachePackageTagsStore": 200,+        "cachePackageTagsRefresh": 60,+        "cacheTemplatesTrack": 100,+        "cacheTemplatesStore": 50,+        "cacheTemplatesRefresh": 15,+        "cachePagesTrack": 200,+        "cachePagesStore": 100,+        "cachePagesRefresh": 10,+        "cachePagesDirtyRead": 10,+        "searchEngineListTemplate": "forSearchEnginesList.htm",+        "searchEngineFileTemplate": "forSearchEngines.htm",+        "searchEngineRobotsDb": "WEB-INF/robots.db",+        "useDataStore": true,+        "dataStoreClass": "org.cofax.SqlDataStore",+        "redirectionClass": "org.cofax.SqlRedirection",+        "dataStoreName": "cofax",+        "dataStoreDriver": "com.microsoft.jdbc.sqlserver.SQLServerDriver",+        "dataStoreUrl": "jdbc:microsoft:sqlserver://LOCALHOST:1433;DatabaseName=goon",+        "dataStoreUser": "sa",+        "dataStorePassword": "dataStoreTestQuery",+        "dataStoreTestQuery": "SET NOCOUNT ON;select test='test';",+        "dataStoreLogFile": "/usr/local/tomcat/logs/datastore.log",+        "dataStoreInitConns": 10,+        "dataStoreMaxConns": 100,+        "dataStoreConnUsageLimit": 100,+        "dataStoreLogLevel": "debug",+        "maxUrlLength": 500}},+    {+      "servlet-name": "cofaxEmail",+      "servlet-class": "org.cofax.cds.EmailServlet",+      "init-param": {+      "mailHost": "mail1",+      "mailHostOverride": "mail2"}},+    {+      "servlet-name": "cofaxAdmin",+      "servlet-class": "org.cofax.cds.AdminServlet"},+ +    {+      "servlet-name": "fileServlet",+      "servlet-class": "org.cofax.cds.FileServlet"},+    {+      "servlet-name": "cofaxTools",+      "servlet-class": "org.cofax.cms.CofaxToolsServlet",+      "init-param": {+        "templatePath": "toolstemplates/",+        "log": 1,+        "logLocation": "/usr/local/tomcat/logs/CofaxTools.log",+        "logMaxSize": "",+        "dataLog": 1,+        "dataLogLocation": "/usr/local/tomcat/logs/dataLog.log",+        "dataLogMaxSize": "",+        "removePageCache": "/content/admin/remove?cache=pages&id=",+        "removeTemplateCache": "/content/admin/remove?cache=templates&id=",+        "fileTransferFolder": "/usr/local/tomcat/webapps/content/fileTransferFolder",+        "lookInContext": 1,+        "adminGroupID": 4,+        "betaServer": true}}],+  "servlet-mapping": {+    "cofaxCDS": "/",+    "cofaxEmail": "/cofaxutil/aemail/*",+    "cofaxAdmin": "/admin/*",+    "fileServlet": "/static/*",+    "cofaxTools": "/tools/*"},+ +  "taglib": {+    "taglib-uri": "cofax.tld",+    "taglib-location": "/WEB-INF/tlds/cofax.tld"}}}
+ test/json-data/geometry.json view
@@ -0,0 +1,1 @@+{"geometry": {"type": "Polygon", "coordinates": [[[-0.939359853061118, 51.57401065198471], [-0.939440892477492, 51.57431080772057], [-0.939543636564569, 51.57460846194951], [-0.939564627628489, 51.574822653966265], [-0.939763179055758, 51.575406210178826], [-0.939787182642249, 51.575552991614], [-0.939819571231525, 51.57564949527348], [-0.939851290904264, 51.57571272360808], [-0.939975682132949, 51.57588648747347], [-0.940036822193859, 51.57598774653439], [-0.940072640734852, 51.57606090267849], [-0.940109693338918, 51.576205104339515], [-0.940125502187331, 51.57645611534466], [-0.940118623499009, 51.57662779459441], [-0.94008851367835, 51.57680555827417], [-0.939998015208524, 51.57709787034017], [-0.939928028667517, 51.577252794653916], [-0.939830628174189, 51.57740747134858], [-0.939796941313687, 51.57749078985784], [-0.939785586773343, 51.57754463746666], [-0.939770656147318, 51.57769016804059], [-0.939658154074368, 51.57824573765943], [-0.939485738834128, 51.57883313597966], [-0.93945717030888, 51.579192545519916], [-0.939524336079755, 51.57928306919007], [-0.939540939284811, 51.57931379092206], [-0.939553921057406, 51.579748206813974], [-0.939549110634237, 51.57983088690364], [-0.939520940649101, 51.58004913048237], [-0.93951863934971, 51.58014801827346], [-0.939525037302583, 51.580245186310094], [-0.939540364690294, 51.58033074581249], [-0.939561798495469, 51.58040197377017], [-0.939594984852117, 51.58046431619436], [-0.939708262827559, 51.580619996404984], [-0.939729069878358, 51.58065615108302], [-0.939759224390201, 51.58072476025555], [-0.939813615319267, 51.58093026195459], [-0.939900523550716, 51.58172321441459], [-0.939894144544868, 51.581997403240464], [-0.939910832471369, 51.58208657162566], [-0.940114353648015, 51.582457967531546], [-0.94022333049887, 51.58267475179605], [-0.940267247821581, 51.58277225843995], [-0.94038427828014, 51.583077233742756], [-0.940493362377966, 51.583289522851196], [-0.940571515796737, 51.58340442254201], [-0.940725857371931, 51.58359464061899], [-0.941198340240742, 51.584069167875626], [-0.941335307610427, 51.584261925920295], [-0.941372786463165, 51.5843261048293], [-0.941416540699353, 51.58443080287741], [-0.941461326187118, 51.584739621364996], [-0.941517445876004, 51.5849954910945], [-0.941575869597484, 51.58533860085252], [-0.941574762730556, 51.58538624677686], [-0.941565185406358, 51.58542572382202], [-0.941550002863301, 51.58545795698674], [-0.941515976834126, 51.58549361692739], [-0.941470510606583, 51.585524677868065], [-0.941423872780384, 51.585544039043356], [-0.94131509921043, 51.585566436558274], [-0.941075026707647, 51.58558675057684], [-0.940953181611661, 51.58561262652464], [-0.940734943851404, 51.58568708689612], [-0.940229533088359, 51.585883040066975], [-0.940067635923264, 51.58595531027499], [-0.939917220191166, 51.58603038143916], [-0.939797255285709, 51.586099433232604], [-0.939675637701456, 51.586177461657186], [-0.939611074554604, 51.58622273608784], [-0.939340396866166, 51.586441486256], [-0.93912244296424, 51.58662744303347], [-0.938817233975319, 51.58671729893312], [-0.938661522913105, 51.586771639765516], [-0.938087678078033, 51.586992141762096], [-0.937332636663603, 51.5873072119485], [-0.93492634290483, 51.58836081134952], [-0.934774110277819, 51.58845114531925], [-0.934663126573178, 51.588505886685766], [-0.934565467487243, 51.588546362183585], [-0.934462140700832, 51.588582290320545], [-0.934337145222589, 51.58861892084846], [-0.934213782000904, 51.588647473549656], [-0.934055887760941, 51.588671216763295], [-0.933259701136762, 51.58864510257983], [-0.933213278816969, 51.588716614209304], [-0.932681740160827, 51.588792708227174], [-0.931992471253759, 51.58900853446031], [-0.930629113767652, 51.58946908647644], [-0.928525036745892, 51.59005953842682], [-0.928533690860491, 51.59024394687689], [-0.928456010745829, 51.59047792078815], [-0.928256501298223, 51.5906748158337], [-0.927916132025856, 51.59066181734799], [-0.927671608556404, 51.590624516635486], [-0.927319567022116, 51.59061680481465], [-0.926850285435352, 51.59062240702726], [-0.926313746570405, 51.59066425819173], [-0.925516790702947, 51.59079184232522], [-0.925387133168341, 51.59078076476673], [-0.925293323156809, 51.59077990606886], [-0.925202187496562, 51.59078806346173], [-0.925112304116411, 51.59080432476366], [-0.925036768134577, 51.59082431403807], [-0.924968299737815, 51.59085066215826], [-0.924899640304234, 51.59088510100373], [-0.924851228495836, 51.590917926896914], [-0.924734051019008, 51.59105082982951], [-0.924694255912291, 51.59108553291224], [-0.924446018071168, 51.59126668934789], [-0.924204972773907, 51.59150995377465], [-0.923492520665307, 51.59215352189589], [-0.922775141969353, 51.59288246146081], [-0.922298613651092, 51.59343737077436], [-0.922101568270595, 51.59365046303728], [-0.92149319027045, 51.59422483935332], [-0.921029930136652, 51.59464499018023], [-0.919455791303858, 51.595519790418045], [-0.919908472816787, 51.59560757835559], [-0.919902358174605, 51.595621908790214], [-0.919721503870405, 51.59588190272939], [-0.919513222918683, 51.59614164398608], [-0.919057326435374, 51.59661490568889], [-0.918991959815932, 51.59669253146064], [-0.918853979663433, 51.59690975873926], [-0.91882720841051, 51.59694278142396], [-0.918711889332325, 51.597056812913756], [-0.918614808732714, 51.59713234806153], [-0.918297245740306, 51.59749448485627], [-0.918073703114389, 51.59778825130676], [-0.917853804369843, 51.59811082427763], [-0.917772408698363, 51.59831598343439], [-0.917709563771812, 51.59853030520902], [-0.917692745480418, 51.59856971355161], [-0.917634249201942, 51.59866178857289], [-0.917443039919903, 51.59887043094418], [-0.917335250471836, 51.598971042984616], [-0.916888284039272, 51.599309503426845], [-0.916921110547038, 51.59938623548714], [-0.916942485633783, 51.5995195094582], [-0.916937652924948, 51.599601289122916], [-0.916945811778801, 51.599683188627004], [-0.916964246289877, 51.599757989575366], [-0.917049297107238, 51.59994580090974], [-0.917089705367533, 51.60000731699611], [-0.917171120773417, 51.60010517795964], [-0.91752823274465, 51.60044925531009], [-0.917834023417531, 51.600765883511485], [-0.917913079603466, 51.600902386385464], [-0.918033251654948, 51.60107073894696], [-0.91851090277833, 51.601507638962644], [-0.918694916056997, 51.601663091331545], [-0.919167224655989, 51.60214309940451], [-0.919302231291783, 51.6024167896932], [-0.919552009673002, 51.60266096432073], [-0.919812571846437, 51.60293760761218], [-0.920091738726093, 51.60358667644501], [-0.920208566517809, 51.603592246724574], [-0.920644522919282, 51.60359715428394], [-0.921093152621479, 51.6036156641763], [-0.921168285587618, 51.603613657034074], [-0.921469264587163, 51.6035867495928], [-0.921584776688781, 51.603586911416876], [-0.921718864440287, 51.603595336217005], [-0.921999521996191, 51.60363387995426], [-0.922136050325758, 51.603661209181794], [-0.922248015684399, 51.60368921195661], [-0.922346882036963, 51.60372159023333], [-0.922478548042792, 51.60377135359644], [-0.922605777045377, 51.603825571929555], [-0.92269948345521, 51.60389297005248], [-0.922855181916126, 51.60396453348124], [-0.922908065211369, 51.60398749776935], [-0.923525984356841, 51.604174796116844], [-0.923689774691443, 51.6042095667925], [-0.923842334751645, 51.60423074681459], [-0.924020987014581, 51.604247669912816], [-0.924152253615336, 51.604253368572344], [-0.924379056519158, 51.60424915231234], [-0.924491916632975, 51.60423939614583], [-0.925129396784027, 51.604149022807746], [-0.925308707040747, 51.60413807581467], [-0.925396812395019, 51.604137083863186], [-0.925816640142735, 51.60415261457396], [-0.926339243350925, 51.604157394450915], [-0.926627592983669, 51.60417621577326], [-0.928219342008724, 51.60433911850777], [-0.928540897883304, 51.60435823822504], [-0.928666454251138, 51.6043611822271], [-0.928925123731038, 51.60435275210506], [-0.929272427371303, 51.60432085222454], [-0.929350681081097, 51.604308977474346], [-0.929525019586135, 51.60426381040009], [-0.929730308556221, 51.60419195021111], [-0.929806393059037, 51.60414948374236], [-0.929854111423592, 51.60408517871234], [-0.929877477717563, 51.604012559228565], [-0.929880189120251, 51.60395863398061], [-0.930157001898001, 51.603362311928585], [-0.930233415197893, 51.60324431822904], [-0.930397156019029, 51.60303540508514], [-0.93058677482955, 51.602831223239896], [-0.930747612253953, 51.60268432562219], [-0.931065151604954, 51.602443542878724], [-0.931358556224333, 51.60224659876122], [-0.931494751600125, 51.602165114969594], [-0.931807666849096, 51.601998019766675], [-0.932115553298882, 51.60186055047904], [-0.932344261944379, 51.601774512081256], [-0.932581062209752, 51.601712824287226], [-0.932837650566022, 51.60166929919289], [-0.93304760157037, 51.60164423223924], [-0.933247236692325, 51.60162806284857], [-0.933524572231589, 51.601623388892406], [-0.93367307135076, 51.60163283011416], [-0.933854562367985, 51.601651562386515], [-0.934222940436163, 51.60170526005669], [-0.934582321335698, 51.60177326158339], [-0.935176916778695, 51.60190903449902], [-0.935323237909383, 51.60194992468383], [-0.935449328012611, 51.60199153042619], [-0.935555166022244, 51.60203475076641], [-0.935754957696255, 51.60213547009268], [-0.935920944220561, 51.60219991608389], [-0.936105992086264, 51.60225194623237], [-0.936251123590137, 51.60228203448254], [-0.9362553703842, 51.602285669620606], [-0.935954861941404, 51.603034649313756], [-0.936025513047831, 51.60328615677915], [-0.937604653681525, 51.60374194595848], [-0.937842729718605, 51.6037494948452], [-0.937998245297976, 51.60376798569096], [-0.938149157504488, 51.60379812385097], [-0.938282536474463, 51.60383709493093], [-0.938398403200176, 51.60388400001471], [-0.938549544157173, 51.60396629131982], [-0.938542113846477, 51.60403725821696], [-0.938540670189083, 51.60403724516617], [-0.938690470772444, 51.60417707075241], [-0.93885671831091, 51.60429276731543], [-0.938949655717409, 51.60433227129961], [-0.939171393952754, 51.60435945147724], [-0.939491406642938, 51.60438302306183], [-0.939580914110842, 51.60438383145672], [-0.939698081584302, 51.604374998744255], [-0.939860253923854, 51.60435578228165], [-0.940131370864495, 51.60430877574863], [-0.940224037565311, 51.60429792302206], [-0.941065905352937, 51.604358569226044], [-0.941456261791824, 51.60439985396626], [-0.94172260946072, 51.604433725629356], [-0.942065117075164, 51.604483568721975], [-0.942405972956444, 51.60454238758566], [-0.942542307804962, 51.60457868292929], [-0.942668453620936, 51.60461848302318], [-0.942754322773386, 51.60465162617505], [-0.942930037674105, 51.6047332338937], [-0.94300827141212, 51.60478429141865], [-0.943050642772121, 51.60482513526179], [-0.943095505071305, 51.604883085645305], [-0.943175837511794, 51.60503037259752], [-0.943199044776033, 51.60508812802956], [-0.943264927978451, 51.605360268787045], [-0.94328295289835, 51.6055168856955], [-0.943285556153251, 51.605653582182974], [-0.94327790338372, 51.60573443814757], [-0.943262026480571, 51.60579633763445], [-0.943228365851772, 51.605877858692274], [-0.943092212068021, 51.60614458434088], [-0.943074097962666, 51.60624063192375], [-0.943061365824471, 51.606415854446915], [-0.943044862244347, 51.60650472320056], [-0.943017897594943, 51.606608783583646], [-0.942979591589439, 51.606703750216774], [-0.942905884027612, 51.60683076806164], [-0.942741267849139, 51.60707925374213], [-0.942655006715899, 51.607249318295594], [-0.942630361430127, 51.60731563455148], [-0.942616263944651, 51.607425205688976], [-0.942621958529523, 51.60749089598715], [-0.942636336634899, 51.60755576531453], [-0.942660904708633, 51.607617129739225], [-0.942696981288364, 51.607680396114986], [-0.942747433102709, 51.60774648940761], [-0.942856244975984, 51.6078490746924], [-0.94299552290573, 51.60794563997599], [-0.943040847342192, 51.60798381288104], [-0.943186973159248, 51.608158666884215], [-0.943285889122319, 51.60825217104693], [-0.943509689787491, 51.60844031193307], [-0.94379690141149, 51.609007570967925], [-0.943824004868445, 51.60908424372538], [-0.943831400135399, 51.609201201602104], [-0.943839541642426, 51.60922375393887], [-0.943852077204152, 51.6092436483043], [-0.943874740412961, 51.60926273459518], [-0.943900416430683, 51.60927645299323], [-0.944008015202393, 51.60930709311221], [-0.944074894993692, 51.60934995527111], [-0.944126815315737, 51.60941516198104], [-0.944143328071964, 51.60945037786937], [-0.944150844233183, 51.609499899486615], [-0.944148608042361, 51.60953404761941], [-0.944074023202655, 51.60969882315075], [-0.94403415265091, 51.60986121332522], [-0.944017357658548, 51.60996266784037], [-0.943992023164905, 51.61030771903426], [-0.943965568458423, 51.61051428886229], [-0.943938629028152, 51.61067949277199], [-0.943919488028492, 51.61075754789674], [-0.943885949921455, 51.61083367520212], [-0.943827406881077, 51.61092935920088], [-0.943739589608875, 51.61104186395993], [-0.943638735204063, 51.61115604969767], [-0.943533757272514, 51.611261206608894], [-0.942952360107171, 51.61179457356613], [-0.942768364001955, 51.61194397664764], [-0.942436714102017, 51.61217027698273], [-0.942010518369458, 51.61248743929258], [-0.941742162181165, 51.61266215641604], [-0.941369087685306, 51.61286829880399], [-0.941218061678922, 51.612965845186196], [-0.941055044244821, 51.613082165678385], [-0.940962318642569, 51.61315685920963], [-0.940802313735133, 51.613329853845215], [-0.940532273923746, 51.613638528438315], [-0.940201080800705, 51.61403027273604], [-0.940058816377521, 51.614185443187054], [-0.939985695447699, 51.614286388653916], [-0.939953592073967, 51.614362527773245], [-0.939906859457064, 51.614508669589505], [-0.939802341993177, 51.61490335803689], [-0.939554942446072, 51.615603370947234], [-0.939452722231603, 51.61596121406953], [-0.939421935085161, 51.61604275989804], [-0.939387192811998, 51.616108085051245], [-0.939207265482904, 51.61639149465681], [-0.939036585027605, 51.61671185322904], [-0.938852844916865, 51.61697274874371], [-0.938765881571911, 51.61710953487875], [-0.938636321443754, 51.617400592062225], [-0.93858227703968, 51.617550263869646], [-0.938563060896774, 51.61763101488344], [-0.938557821588965, 51.617793716095726], [-0.938568952285691, 51.6178738422429], [-0.938586194898738, 51.61793963701825], [-0.93864824402059, 51.61806608070808], [-0.938712639101221, 51.61815388154161], [-0.938801458484915, 51.61824729803767], [-0.938902019778203, 51.61833272809444], [-0.939001577491258, 51.61839926656718], [-0.939080060597956, 51.6184404379885], [-0.939144458958051, 51.618466196342986], [-0.93924483456823, 51.61849767462955], [-0.939389936057716, 51.61853135511866], [-0.939533886937889, 51.618552436752374], [-0.939605967169062, 51.618558482633226], [-0.939704125343146, 51.618561167280205], [-0.940011869234288, 51.61855765138392], [-0.940970156479417, 51.61846828865034], [-0.942814559287174, 51.61834913395653], [-0.9433943585448, 51.61832378069166], [-0.943791844341078, 51.61831207069643], [-0.944095316626396, 51.61830580819228], [-0.944345189840605, 51.61830625638639], [-0.944633949874169, 51.618311549349706], [-0.944866264500346, 51.618321729423634], [-0.945069571918896, 51.61833704343469], [-0.945259736594118, 51.618358533188456], [-0.945405071369022, 51.6183823175338], [-0.945538707551366, 51.61841229079249], [-0.945670733169093, 51.618449442740584], [-0.945789657802543, 51.61849097274293], [-0.945898390514756, 51.618536007789324], [-0.946002624502939, 51.61858819565799], [-0.946144975691139, 51.61867849034559], [-0.946290660655923, 51.61881197458077], [-0.946424293873759, 51.618966930407886], [-0.94648587943588, 51.619052004215185], [-0.946597289998464, 51.61923103785771], [-0.946748500992991, 51.619500344620654], [-0.946865282768134, 51.619759451627154], [-0.94692090631825, 51.619852564183816], [-0.946986743272162, 51.619941272483025], [-0.947121992710119, 51.62008904866086], [-0.947221067183078, 51.620177155655796], [-0.947357337089385, 51.62028088174252], [-0.947636341593443, 51.62045871900467], [-0.947732020809812, 51.62050633282523], [-0.947832261440868, 51.62054409665008], [-0.948024909405708, 51.6205835873772], [-0.948272727755043, 51.62061098363029], [-0.94903310890345, 51.62064836261257], [-0.949342820854191, 51.62068530212313], [-0.949548509996597, 51.62072310869445], [-0.949603327033706, 51.620726296510135], [-0.94966121922192, 51.62072141934501], [-0.949725095662238, 51.62070760405018], [-0.949813979767225, 51.620674230793945], [-0.949850856696741, 51.620766275113425], [-0.949938127431095, 51.62092800552347], [-0.95012170684362, 51.62142328688538], [-0.950137689181391, 51.621481875301626], [-0.950144591409982, 51.621558365780544], [-0.950143525838059, 51.62166715485939], [-0.950086848280287, 51.62218276713024], [-0.950081566444785, 51.62234906486587], [-0.950091380322934, 51.6227375905417], [-0.950012857804674, 51.623198158520914], [-0.949963053357781, 51.62366617649237], [-0.94993621019599, 51.62382778558582], [-0.949887611273865, 51.62405573809743], [-0.94981748429385, 51.62434014525264], [-0.949698012983878, 51.6246969431444], [-0.949601071539472, 51.62520410239216], [-0.949519655994856, 51.62547671925938], [-0.949509640700394, 51.62553507516918], [-0.949509569744091, 51.62560071332227], [-0.949517625184728, 51.6256898023632], [-0.949539397678852, 51.62581048474923], [-0.949602831958219, 51.626066413978656], [-0.949622600072634, 51.6262113557699], [-0.949625807500304, 51.62644786389373], [-0.949588359660548, 51.62688092470566], [-0.949587162842075, 51.62718303218221], [-0.949593421398238, 51.627287390862], [-0.949641886384285, 51.62762860642186], [-0.949677564559904, 51.627772791296934], [-0.949732061930258, 51.62791534613564], [-0.949847071945823, 51.62812767746571], [-0.95005020451512, 51.62890367131774], [-0.949860358893705, 51.62924185674224], [-0.949849459019953, 51.62927592739189], [-0.949848649628705, 51.62931098742953], [-0.949857806222275, 51.62935243070728], [-0.949882540677514, 51.62940750068883], [-0.949782851646605, 51.62940750843079], [-0.949684104107472, 51.6296170289926], [-0.949619670293909, 51.62984214210067], [-0.949589348281571, 51.62990391300069], [-0.949478823262764, 51.63006027755304], [-0.949419643728549, 51.63018293320961], [-0.949378577882241, 51.63033452402259], [-0.949364251310331, 51.63051692549393], [-0.949339228920643, 51.63059942443184], [-0.949248633732607, 51.63076855530058], [-0.949181283974238, 51.630869557872636], [-0.949044504574139, 51.63103647701982], [-0.948893935504611, 51.63136242402941], [-0.948761069414249, 51.631297394415576], [-0.948669492362367, 51.63125880992652], [-0.948537801268757, 51.63120547966423], [-0.948447461288637, 51.63117589768262], [-0.948353982967507, 51.63115707745748], [-0.948302104865814, 51.63115121794117], [-0.948079606840815, 51.63115102360314], [-0.947964190602531, 51.63114369566099], [-0.94782149485015, 51.63112892992514], [-0.947725107052886, 51.63111098229264], [-0.947684910040859, 51.63109983219839], [-0.947483021604023, 51.63102069524269], [-0.947447137482345, 51.63101048287877], [-0.947306094892948, 51.630986739722026], [-0.947000710620611, 51.63094713644845], [-0.946674033898238, 51.6308911565172], [-0.946364247153943, 51.63079216747898], [-0.946230490849763, 51.63076579091048], [-0.946172752135344, 51.63076347450912], [-0.94612066649297, 51.630766603772976], [-0.945914796224295, 51.63079712600587], [-0.94583241748209, 51.63079818492752], [-0.945382953779145, 51.63067905708907], [-0.945085984219956, 51.63058827220451], [-0.944883205963743, 51.630547786680644], [-0.944304065189339, 51.630538086402446], [-0.94394898927073, 51.630523204990325], [-0.943611477254356, 51.63049858971805], [-0.943603681926519, 51.6307098224617], [-0.943277115569317, 51.63102249010073], [-0.943359247728321, 51.63109426279834], [-0.942862682241593, 51.631385618132356], [-0.942777562378065, 51.63144239822567], [-0.942532313252267, 51.63161282917167], [-0.942337892928893, 51.63177202827151], [-0.94226606523137, 51.631816339444626], [-0.942197377359527, 51.63184988892168], [-0.942115813951467, 51.631877927416205], [-0.941937005967162, 51.631924871353945], [-0.941512981586086, 51.63201636159427], [-0.94144898217334, 51.6320346671529], [-0.94125937051438, 51.632111184999914], [-0.940790185204025, 51.6323425346351], [-0.940501042462563, 51.632535943664614], [-0.940243238615803, 51.63268647504363], [-0.940131608045529, 51.632764593869275], [-0.939328651391563, 51.63361514453957], [-0.939011405652477, 51.634145481445906], [-0.93882930544315, 51.63451878632893], [-0.93850479164515, 51.63486472819315], [-0.93841585373771, 51.63483694950617], [-0.938308667759282, 51.63503379600045], [-0.938237520065274, 51.63511048065125], [-0.938174497877949, 51.635148574806536], [-0.938097155068085, 51.63518114448376], [-0.938014243480848, 51.63520467214815], [-0.937885103100742, 51.635227781700706], [-0.937899482630853, 51.63529265131639], [-0.937852791962712, 51.635312010611116], [-0.937756752174709, 51.63534081433219], [-0.93768849662133, 51.635355482713884], [-0.937445207164159, 51.63537845841074], [-0.937282775985471, 51.6354039635962], [-0.936848828519967, 51.635485456519916], [-0.935951432459366, 51.63561220386278], [-0.935719677036262, 51.635636179646525], [-0.935420148631615, 51.63565414571281], [-0.935249454884316, 51.63560044703531], [-0.935077590540852, 51.635535048401415], [-0.934717727795324, 51.63535285185533], [-0.934413155884105, 51.635216113905294], [-0.934240481559584, 51.635123731852076], [-0.934130824273522, 51.63505529964683], [-0.934018404467151, 51.63498144729724], [-0.93379675026591, 51.63482118293064], [-0.933679892303923, 51.6347517857655], [-0.933595523374065, 51.63471415412531], [-0.933350473747258, 51.63462740768713], [-0.933261876876166, 51.63458524160035], [-0.933052343088727, 51.63446285066096], [-0.932887751506176, 51.63439571653097], [-0.932638117941304, 51.63431971687678], [-0.932307197466639, 51.63419801967628], [-0.931343887912331, 51.63380981222079], [-0.931071830619705, 51.63376597498289], [-0.930956833347264, 51.63374065092008], [-0.930823331188951, 51.63370346918478], [-0.930701576222194, 51.633658301797674], [-0.930601553796263, 51.63361063468632], [-0.930506119022842, 51.63355221932683], [-0.930386739592999, 51.633467510133144], [-0.930286034494439, 51.633387466714524], [-0.930225334024314, 51.63332666999925], [-0.930188937852857, 51.633276884582266], [-0.930114567680391, 51.63312155131756], [-0.92999324620862, 51.63293521875163], [-0.929900400330813, 51.63282827174111], [-0.929602366751531, 51.6325378240622], [-0.929499431873266, 51.632429885607195], [-0.929341000864788, 51.63222433167361], [-0.929059923028374, 51.631889079042224], [-0.928963213558906, 51.63176231446386], [-0.928896162303318, 51.6316645934755], [-0.928844852932023, 51.631573310156845], [-0.928688841592085, 51.63126527289447], [-0.928672375509884, 51.6312282570475], [-0.928615004479859, 51.631026321392014], [-0.928599834825331, 51.63099561149414], [-0.928552267379979, 51.63092953870757], [-0.928477466267079, 51.630854225808044], [-0.928389580173335, 51.630782390113474], [-0.928294323595921, 51.6307167812279], [-0.928190294396403, 51.63065558801307], [-0.928078937075949, 51.630598823630024], [-0.927494137115659, 51.630339021667446], [-0.927177150281751, 51.63017967221046], [-0.927053070740772, 51.63011110151437], [-0.926922044883713, 51.63003077810591], [-0.926774152423019, 51.6299305188448], [-0.926569924361936, 51.62976860145066], [-0.926465515994788, 51.62966244509719], [-0.926222881099496, 51.62935271335746], [-0.925807083215573, 51.62885077488713], [-0.925685153671251, 51.62869140695483], [-0.925627779063265, 51.628612654944924], [-0.925107975398826, 51.62780494620014], [-0.925039238852947, 51.627717997425265], [-0.924853081975605, 51.627525670804864], [-0.924566287633664, 51.62725149768458], [-0.924018135317732, 51.62666651663998], [-0.9236755124427, 51.62631000590676], [-0.923506463123086, 51.62618886774415], [-0.92325024557972, 51.62602736696645], [-0.923179326155173, 51.62597186775246], [-0.923151247824846, 51.62593744208942], [-0.923025917353845, 51.62573937605691], [-0.922746731858051, 51.625388839233764], [-0.922647729925315, 51.625421200084226], [-0.922438316532782, 51.62517470633975], [-0.922266691774008, 51.62491866820555], [-0.922176577184834, 51.624819832363215], [-0.921722712186069, 51.62440384906751], [-0.921324388846087, 51.624082786314446], [-0.921078577970903, 51.623909687134365], [-0.920860652296657, 51.62377910436709], [-0.920574889783577, 51.623646099345635], [-0.920239048968319, 51.623492851580735], [-0.920014292502202, 51.62371107994427], [-0.919774493442185, 51.623954346092496], [-0.9196346398006, 51.62406455586703], [-0.919395541436808, 51.62427815528515], [-0.919291601043514, 51.624396787581134], [-0.919216417933559, 51.6245210794288], [-0.919153464283899, 51.62467785367513], [-0.919134125069682, 51.624762197061855], [-0.919115026043568, 51.62495803894169], [-0.919049238753011, 51.62517323267518], [-0.919009465019033, 51.625266379578875], [-0.918926652164549, 51.625407685106715], [-0.91889138185638, 51.62549368011671], [-0.918887531877765, 51.62553410703284], [-0.918896749679156, 51.62569334383204], [-0.918892707420934, 51.62574186144837], [-0.918872585162246, 51.62579832350382], [-0.918836276012485, 51.62586722482348], [-0.918763089019947, 51.62596815648993], [-0.918486277330256, 51.626309088016214], [-0.918317614737411, 51.62653682130949], [-0.917905561785174, 51.626975413689514], [-0.917718470441796, 51.62724883363936], [-0.917588198962995, 51.627380709193844], [-0.917518986462533, 51.627435819379606], [-0.917391270738013, 51.627520961779815], [-0.916852199941663, 51.62783249660931], [-0.916708494011591, 51.62822500463541], [-0.916564194300437, 51.62882431452802], [-0.91642514281193, 51.62950639562586], [-0.916399138398425, 51.62974892972319], [-0.916394191477189, 51.62995659074321], [-0.917260413231125, 51.62992321851857], [-0.917422640699178, 51.63166639247214], [-0.91745600722272, 51.63232848375385], [-0.917204699151121, 51.632324368705305], [-0.916787813832452, 51.632356490901394], [-0.916473041038736, 51.63240843616508], [-0.91615824611563, 51.63246127954521], [-0.916096846672852, 51.632491284427324], [-0.916000979824314, 51.632573122584574], [-0.916035541448316, 51.63300234215989], [-0.916058746179642, 51.633180590521675], [-0.916070697537387, 51.63322475979131], [-0.916098260729099, 51.63328076225654], [-0.916287121367376, 51.6334803208717], [-0.916556537933578, 51.63369500893812], [-0.916833436931067, 51.63389897542989], [-0.916980624704717, 51.63396776999687], [-0.917236275570907, 51.634093312450645], [-0.91746761535009, 51.63414759643489], [-0.917599467742121, 51.63382870989293], [-0.917652284973834, 51.63367364156546], [-0.917677934007693, 51.63362802065708], [-0.917745133700401, 51.63359716923921], [-0.917749446066394, 51.633598108140106], [-0.917778102484095, 51.633608262991594], [-0.918084580428658, 51.633662338963], [-0.918258883815884, 51.633684625273325], [-0.918506861448772, 51.63370758977881], [-0.918953829589691, 51.63374767133924], [-0.919245145699076, 51.633771033193085], [-0.919300707852053, 51.63386505735126], [-0.919518444759651, 51.63449197843591], [-0.919277685382965, 51.63446908250848], [-0.919031520477806, 51.63467362436313], [-0.918786199566619, 51.63490335001199], [-0.918632752044247, 51.63503681158531], [-0.918623327237537, 51.63512933850577], [-0.918590569006466, 51.635413172104], [-0.918679820058316, 51.63554886817429], [-0.918329488250497, 51.63557621368664], [-0.918276435808259, 51.635741170885815], [-0.918247752304029, 51.6360358318421], [-0.918226764712395, 51.6364321689642], [-0.918316733144818, 51.63702374696847], [-0.918287332378286, 51.63783452012518], [-0.918306713699577, 51.63835621240955], [-0.918316674296085, 51.639213205140855], [-0.918347519668651, 51.639799742499974], [-0.918322169732004, 51.63995416480466], [-0.918318885834364, 51.64003146244247], [-0.91832686087795, 51.64012145202931], [-0.91837911416845, 51.640354816042915], [-0.918418495846573, 51.64070405330022], [-0.918530296534871, 51.64128953760163], [-0.918512952473936, 51.64147190762244], [-0.918367217757134, 51.64200916315153], [-0.917905817511154, 51.6420255938785], [-0.917874522823857, 51.64200462484983], [-0.9178264462409, 51.64196012300076], [-0.917792775459127, 51.641917552189355], [-0.917773553228143, 51.64187511449974], [-0.917765526057023, 51.64184806570082], [-0.917757894232724, 51.6417436926823], [-0.917749532884981, 51.64142711089125], [-0.917739557627689, 51.64136048104305], [-0.917720785029217, 51.641299165096676], [-0.917688816437575, 51.64124581999865], [-0.917649452591449, 51.641199600032465], [-0.917568381483579, 51.64114490323613], [-0.917433423830488, 51.64098630616572], [-0.917315173490429, 51.64079369481265], [-0.916601390868836, 51.640243120219885], [-0.916329281834837, 51.640018517003504], [-0.916001068304932, 51.63966211774633], [-0.915790319174844, 51.63947044935813], [-0.915502073944432, 51.63913509831614], [-0.915393357554307, 51.63902799332443], [-0.915169439572157, 51.63884339546647], [-0.914312592230519, 51.638234837864296], [-0.913969150495335, 51.63797270396478], [-0.913808719070197, 51.63773204326735], [-0.913709021859032, 51.63761063348384], [-0.913238840648949, 51.637814889395024], [-0.912211197236081, 51.63818662075986], [-0.912057780107923, 51.6382571327065], [-0.911911629192503, 51.63832591344631], [-0.911683066608314, 51.6384568717294], [-0.911571655864317, 51.638583520328496], [-0.910821072629623, 51.63897039510019], [-0.910799442348496, 51.63890815232427], [-0.910457034748542, 51.63884473254883], [-0.90959863753988, 51.63872527019996], [-0.909397970101408, 51.639078576087414], [-0.909048252097236, 51.63925965643008], [-0.909362542404515, 51.63959166868019], [-0.90780907162756, 51.64013291512084], [-0.906217399025103, 51.640939038154336], [-0.905898125060113, 51.64111499768922], [-0.905490384709512, 51.641363863281036], [-0.905329841704473, 51.64154939281248], [-0.905189541321435, 51.641794455508446], [-0.90489098319803, 51.64194992468491], [-0.904781623731163, 51.641990266433346], [-0.904676706895303, 51.64202615370086], [-0.904609919053975, 51.64203901826923], [-0.904237228330216, 51.64209218907456], [-0.90337364044862, 51.64224597828853], [-0.902975191138407, 51.642348358507896], [-0.90292303291413, 51.6422939216028], [-0.902563354675933, 51.64240655335107], [-0.902346768622897, 51.642458479170756], [-0.900824166346957, 51.64291090498388], [-0.900831401206159, 51.64297031739377], [-0.900456613238591, 51.64299018760407], [-0.900019098619797, 51.64297440185565], [-0.899740380937974, 51.64290615240652], [-0.899740489711103, 51.64290165761423], [-0.899316872364163, 51.642849133775655], [-0.899203336991784, 51.642822893203494], [-0.898949902655656, 51.64272520647125], [-0.898783095909619, 51.642631028913314], [-0.898613530793898, 51.64253143027416], [-0.898274969154989, 51.64230885935864], [-0.898003026265053, 51.642080618381755], [-0.897815191816853, 51.641840577798526], [-0.897681652652772, 51.6416262230417], [-0.897327401428262, 51.64091885338664], [-0.896908641882592, 51.6401308512027], [-0.896689777704333, 51.63985994571793], [-0.896199107519928, 51.639356299928586], [-0.896147069948886, 51.63929736526477], [-0.896071667668875, 51.63912941223961], [-0.896034388155249, 51.63881974973283], [-0.89604243255033, 51.63866696761571], [-0.896079873457203, 51.63849378104931], [-0.896158355572289, 51.63829670279626], [-0.896345617779906, 51.63796397393339], [-0.896436991088377, 51.637771512438086], [-0.896627579786388, 51.63748017587825], [-0.896853716275347, 51.63721255114492], [-0.896930541034958, 51.63714313797598], [-0.897052325259386, 51.63706695367484], [-0.897622203318074, 51.636806151704704], [-0.897875248140837, 51.636679946210485], [-0.898045509693898, 51.63657094674504], [-0.898161794997489, 51.63648302058021], [-0.898300490282884, 51.636364732965696], [-0.898761753797595, 51.63587451909389], [-0.898944791614438, 51.635595696246455], [-0.899167964890093, 51.63497197055003], [-0.899296205851425, 51.63450920414348], [-0.899574174378538, 51.633591065468096], [-0.89965934952667, 51.63323579470252], [-0.899745200652967, 51.632733067337156], [-0.89977291429982, 51.632065248259224], [-0.899749317297925, 51.63190587518938], [-0.899730833669311, 51.631833768913715], [-0.899698165870897, 51.631750739777836], [-0.89965967663145, 51.63166945441679], [-0.899415121605884, 51.63126703489972], [-0.899342021743563, 51.6311233827535], [-0.899274678557411, 51.630980683675816], [-0.899221406706553, 51.63067446858268], [-0.899099405183406, 51.63046202142283], [-0.899036514315775, 51.63037421293725], [-0.898969246754073, 51.63028816171176], [-0.898790586842518, 51.63008867047265], [-0.898592010806669, 51.62987640386868], [-0.898497321924236, 51.62978919602221], [-0.898330098469121, 51.62965455114822], [-0.898092642525963, 51.62949676796704], [-0.897090362270362, 51.62894426419184], [-0.896737033461809, 51.62879887742956], [-0.896347312922056, 51.6286648367393], [-0.895633444660858, 51.62837398981453], [-0.895432402381131, 51.628264199314984], [-0.895328663542796, 51.62819308870188], [-0.895257753282041, 51.62813847178444], [-0.895144755729953, 51.62803220648194], [-0.894948239644532, 51.627795675481295], [-0.894944118635818, 51.62766795529761], [-0.894659948504904, 51.62723368184], [-0.894597856997988, 51.62711350846984], [-0.894557343909314, 51.62699713484451], [-0.894469809950127, 51.62655661915932], [-0.894297931114171, 51.625960652861906], [-0.894027819576293, 51.62530531541029], [-0.893931965007508, 51.62514795788166], [-0.893770350871199, 51.62496210671055], [-0.893637995566781, 51.62476124514902], [-0.893507481839104, 51.62466290563429], [-0.892108104715385, 51.62369479825078], [-0.891788795844237, 51.623459800649094], [-0.891922368328922, 51.62337294285213], [-0.891402229815371, 51.62296161084893], [-0.891224922435199, 51.622768414566416], [-0.890089770742591, 51.621276764770215], [-0.890188416566163, 51.62120036880181], [-0.889947380146256, 51.62101466068101], [-0.889727152716072, 51.62086421627997], [-0.889328903798693, 51.62066892786315], [-0.889153387824523, 51.620758083474584], [-0.888409981594066, 51.6201459098237], [-0.888016456293258, 51.619876030785385], [-0.887588982852847, 51.619754191291285], [-0.887143566599165, 51.61965735685454], [-0.88701981132148, 51.61963820065132], [-0.886790744842007, 51.619613550199404], [-0.886603228328527, 51.619602780759124], [-0.886388075513318, 51.619599841439666], [-0.886090409498186, 51.61960421155123], [-0.8851368584814, 51.61967159165818], [-0.883828125903616, 51.61979133688442], [-0.883515973065361, 51.619797361107345], [-0.883424375160774, 51.6198216668555], [-0.883259776293847, 51.619877648328995], [-0.883163867754863, 51.61990101371408], [-0.883044634749604, 51.619815357981636], [-0.881854770311083, 51.61956485587701], [-0.881704286246311, 51.61951666620604], [-0.881635675440863, 51.619487239450166], [-0.881560131370525, 51.619446057447796], [-0.881477720403852, 51.619390423316226], [-0.881374708667671, 51.619291432867335], [-0.880834298239682, 51.61853547912722], [-0.880617549323506, 51.618127888748454], [-0.880388345702594, 51.61816796449318], [-0.879226788544387, 51.6185884841998], [-0.878534686661489, 51.61883184500677], [-0.878385021629543, 51.61898507235025], [-0.878148819949825, 51.61913297697072], [-0.878102165244257, 51.61915051459153], [-0.87804120240859, 51.61916252048532], [-0.877967464227806, 51.619165412606506], [-0.877896924603018, 51.61915574690363], [-0.877839648002824, 51.61913541789456], [-0.877812586845032, 51.61911987344738], [-0.877382402665225, 51.61869944717381], [-0.877261875140598, 51.61866682400238], [-0.877087293116294, 51.618600414623565], [-0.876592344555959, 51.61846350297477], [-0.876408990128558, 51.61845995037962], [-0.876374465270622, 51.61845422502283], [-0.876347205240439, 51.61844677083749], [-0.87630299358364, 51.61842386858449], [-0.876241963569583, 51.618380124514495], [-0.876216636094276, 51.618352907107045], [-0.876152781748704, 51.618131100785114], [-0.876106882196741, 51.61811807314475], [-0.876305643178539, 51.617614643124426], [-0.875030363348109, 51.6174927341714], [-0.874705473526163, 51.61748872204597], [-0.874564111923802, 51.6174810731233], [-0.874279482039409, 51.61730930128958], [-0.874259717266618, 51.61699889890464], [-0.874416434195306, 51.61673604585861], [-0.874423036602998, 51.61652750219062], [-0.870144618410033, 51.61769939147719], [-0.870074692270606, 51.617607003497994], [-0.869665459226217, 51.61768399010514], [-0.869137937974611, 51.617812887107476], [-0.868711126950696, 51.61789959196275], [-0.868460959800069, 51.617969115035805], [-0.866874516019656, 51.61827841686334], [-0.866642434191741, 51.6183175388698], [-0.866517885416195, 51.61833072340073], [-0.866392071649004, 51.61833670224208], [-0.865776872894858, 51.61833166083683], [-0.865633662902087, 51.618340168339564], [-0.865493072065054, 51.618359490991885], [-0.864874419821269, 51.618492883413026], [-0.864829070270363, 51.61851582340227], [-0.8648101986804, 51.61846169086974], [-0.864778464993556, 51.618402038980314], [-0.864733824417545, 51.61833866562683], [-0.864681918741689, 51.61827702035103], [-0.864649512487616, 51.61824433697217], [-0.864492159027339, 51.61812502442397], [-0.864510219622979, 51.618037979732996], [-0.864490008258273, 51.61792179156664], [-0.863791866117017, 51.616668790850674], [-0.863578496430752, 51.61619016667409], [-0.863939572338851, 51.6160749702936], [-0.864141702322916, 51.61602027805823], [-0.864343742093738, 51.615969181278444], [-0.864684853799959, 51.61590144547291], [-0.864287415633653, 51.61573755014447], [-0.864114344880943, 51.615611790807684], [-0.86394143216696, 51.615479738556914], [-0.863424226660599, 51.6150224515856], [-0.863108525016747, 51.61470918210108], [-0.862999136822108, 51.61463618943345], [-0.862947760773663, 51.614611414387845], [-0.862827479233939, 51.614569786988696], [-0.862764327092327, 51.6145538895073], [-0.862690954996651, 51.614542388851355], [-0.862422671693898, 51.614528100870835], [-0.862278208948609, 51.61452939875511], [-0.862025067586916, 51.614544929155834], [-0.861633333855888, 51.61461576315295], [-0.861195617216796, 51.61467715820802], [-0.860991546767721, 51.61469406144679], [-0.860819513274215, 51.61470048511226], [-0.860591345418716, 51.61469917048917], [-0.860366335624517, 51.61468709604235], [-0.860199196800327, 51.61467108707321], [-0.860140174693704, 51.6146633207896], [-0.859842784619867, 51.61460018922178], [-0.85969929013584, 51.61456282905675], [-0.859501466379857, 51.61450336097874], [-0.859306643699259, 51.614439425872085], [-0.859175017225196, 51.614389591981386], [-0.858977780895874, 51.61430675031806], [-0.858172868778381, 51.61392127488393], [-0.856305510909038, 51.61299763777437], [-0.855879431428414, 51.61282624136621], [-0.855413827571698, 51.612676038205095], [-0.855073660630441, 51.61259179657028], [-0.854915624510577, 51.612558784566126], [-0.854688372582268, 51.61252150056368], [-0.854459541396129, 51.6124895957323], [-0.854208848612115, 51.61246556965946], [-0.853413812340721, 51.612436228916096], [-0.851724243907319, 51.61242871018406], [-0.851450292115, 51.612410744947404], [-0.850998615679464, 51.61233888823236], [-0.850653274334699, 51.61223120468302], [-0.850372939737194, 51.61235164666859], [-0.850315593337778, 51.61233490012849], [-0.850193999216673, 51.612288751000705], [-0.850146601119627, 51.612278395984944], [-0.849926028861458, 51.61226274823948], [-0.849854084982468, 51.61225215268858], [-0.849714847013531, 51.612218418620564], [-0.849473435162227, 51.61217019573735], [-0.849372523204365, 51.61216291272777], [-0.849235958321253, 51.61219484371945], [-0.849170915863905, 51.6121969037703], [-0.849112126238191, 51.61218014249445], [-0.8487984760956, 51.61201881385575], [-0.848813983178796, 51.61186251023449], [-0.848805967905994, 51.611779707971536], [-0.848777693193183, 51.61169850541887], [-0.848751669639592, 51.61164250170296], [-0.848740414389996, 51.61163070213332], [-0.848685616135578, 51.61162746733029], [-0.848598109857592, 51.61148993537107], [-0.848561752293423, 51.61144282198033], [-0.848420551583887, 51.61131555337684], [-0.848346143135118, 51.61123120073643], [-0.848017978126262, 51.61073074025961], [-0.84803240586268, 51.61067423405594], [-0.848009203184881, 51.61062095534751], [-0.848010806265629, 51.61061467688053], [-0.848360723390725, 51.61054078070849], [-0.848731046077576, 51.61040234328455], [-0.848814480324059, 51.61035820278237], [-0.848820437461021, 51.610351067821576], [-0.848690278305866, 51.610072847084595], [-0.847947554822518, 51.60900543974003], [-0.847435103308081, 51.6083125448088], [-0.847167998407497, 51.60796823756615], [-0.846186886755928, 51.60662333116728], [-0.845549998419521, 51.605828497430636], [-0.845383680959569, 51.60566770865375], [-0.844840468159951, 51.60522267062485], [-0.84443530295085, 51.60497141150611], [-0.844232696249957, 51.60481835643492], [-0.843486943931864, 51.60416181103064], [-0.84332558359401, 51.60420428093958], [-0.842908147301968, 51.603868373383044], [-0.842663709069561, 51.603714004109015], [-0.842310199280505, 51.603533382087996], [-0.841471682375652, 51.60311958453644], [-0.841215384127901, 51.602977683579475], [-0.840873068217364, 51.60281155440105], [-0.840467421086961, 51.602637605395635], [-0.840268344470896, 51.602516948405764], [-0.840203154295604, 51.60246864832527], [-0.838474883614574, 51.601841924417336], [-0.838334815858368, 51.60178568911777], [-0.838125139250388, 51.60168470536249], [-0.837520083534699, 51.60134782062488], [-0.837365310402971, 51.60124558073874], [-0.837143016970701, 51.6010734356079], [-0.83702163004049, 51.600964332538595], [-0.836879440722268, 51.60082175386177], [-0.836733036616179, 51.60067463738169], [-0.836105420124444, 51.59992210370809], [-0.836001567218505, 51.59980508080317], [-0.835894758960057, 51.59969072601903], [-0.835646409012494, 51.5994652683849], [-0.835572705263831, 51.59941148620318], [-0.835355212679077, 51.599278049641256], [-0.835001490423373, 51.59910819321327], [-0.834666328550124, 51.59894661249016], [-0.834023851532118, 51.59860753956394], [-0.833813699592692, 51.59846967713012], [-0.832219972267628, 51.59727861587006], [-0.831975375091702, 51.59713321406168], [-0.83180286996819, 51.597047874561696], [-0.83156138306724, 51.59695015903964], [-0.831279067386554, 51.596868221715724], [-0.831119729288761, 51.59683246683902], [-0.830996129682144, 51.596810555210986], [-0.830798964246538, 51.59678611250204], [-0.829935838169309, 51.596720865686486], [-0.82967342169104, 51.596707460026586], [-0.829378950815281, 51.59670542343618], [-0.828672101908656, 51.596738836236966], [-0.828529092462618, 51.59674190522934], [-0.828241907711544, 51.596737240965595], [-0.828066024571149, 51.59672739285258], [-0.827854311754233, 51.596707295846045], [-0.827399248313258, 51.59666228882735], [-0.827164974703315, 51.59662128423301], [-0.827063074447412, 51.59659778687348], [-0.826830893268571, 51.59653162563389], [-0.826403126611598, 51.596379885757386], [-0.826320361645679, 51.596342192238744], [-0.826196596762853, 51.596270819297594], [-0.825930135887572, 51.596078429107884], [-0.825702044863314, 51.59590980062742], [-0.825239717878873, 51.595531118786504], [-0.824901250965003, 51.59522021379687], [-0.824412039116432, 51.59470908081712], [-0.8242790371805, 51.59460434384618], [-0.824162427084317, 51.59453573788591], [-0.823734544991122, 51.59433363318264], [-0.822923577534385, 51.59387232098135], [-0.82274220720895, 51.593740122024236], [-0.822758790294358, 51.59343366962803], [-0.822450529949737, 51.592123177421634], [-0.822408018987733, 51.591980681321466], [-0.82236394886053, 51.59184266543808], [-0.822242174035691, 51.59158338051977], [-0.821801765862476, 51.5908065697487], [-0.821372709449585, 51.59009371260957], [-0.821171850754746, 51.58982284235581], [-0.820679968304875, 51.58936291919855], [-0.820417155492676, 51.58914357740762], [-0.82015811620844, 51.5888901042506], [-0.820045903377484, 51.58876399099103], [-0.81975498206002, 51.5884040936899], [-0.819793038416411, 51.58838379523831], [-0.819648800144719, 51.588268149727725], [-0.819147039514258, 51.58763368041164], [-0.818936357700162, 51.58740856531028], [-0.818746726401906, 51.58698404449756], [-0.818744261992127, 51.58691208559554], [-0.818786542624285, 51.586561832259704], [-0.818798419467402, 51.58638211648965], [-0.818772601288344, 51.58593047014409], [-0.818622316220491, 51.585380460186784], [-0.818493730937228, 51.58471737211902], [-0.818460174123387, 51.58461992329734], [-0.818316632401033, 51.58436670899539], [-0.818092592164715, 51.58404524580265], [-0.817774082795079, 51.58341531231479], [-0.817553796530972, 51.58311636524129], [-0.817445537064436, 51.58300557512581], [-0.81689125274485, 51.582562990062094], [-0.816348587477007, 51.58217357091916], [-0.816214612729635, 51.58210837837547], [-0.816144330954904, 51.58209148442308], [-0.816059129697027, 51.58209332267878], [-0.815742672724781, 51.5820523651453], [-0.815217212787465, 51.58194545592985], [-0.815002344756643, 51.58188304223224], [-0.814826435408141, 51.5818210214772], [-0.814485295861823, 51.581730356582035], [-0.81444044205173, 51.581790148477246], [-0.813566956536122, 51.581635656767844], [-0.813282023122958, 51.58154825394594], [-0.813052037332166, 51.581456910143594], [-0.812870182657061, 51.581346270732034], [-0.812790810356639, 51.58129061819263], [-0.812604263086127, 51.581138568732094], [-0.812386509085548, 51.58085492331321], [-0.812249092317067, 51.58070067277682], [-0.812167619564544, 51.58055957666617], [-0.812051961231651, 51.58029045130041], [-0.812042798783181, 51.580254391465495], [-0.812034433182585, 51.580187767648724], [-0.812033188044369, 51.58012481260171], [-0.812058064730261, 51.580000977815104], [-0.81229781440549, 51.579440518478464], [-0.812247901650195, 51.57941753400863], [-0.812446355706805, 51.57916777205078], [-0.81277233113049, 51.578787120404634], [-0.813191367818544, 51.57821318632392], [-0.813476583358647, 51.57767926033569], [-0.81372993476285, 51.57714950751218], [-0.813894821439398, 51.57669169369027], [-0.813998634879093, 51.57614064720803], [-0.814138744624452, 51.57508193151095], [-0.814120940539715, 51.57493428638376], [-0.81419874804071, 51.57321943977239], [-0.814492542549023, 51.572575897898666], [-0.814547744449169, 51.57211787423915], [-0.814549364628156, 51.57200009817749], [-0.81449843948966, 51.57157247390041], [-0.81447275908744, 51.57022974030085], [-0.814470739157426, 51.56964165674743], [-0.814429823166273, 51.56938407823071], [-0.814416473674158, 51.5693425811485], [-0.814362577800679, 51.569251219648685], [-0.81406844007837, 51.5689659058573], [-0.813908308238922, 51.56874308971205], [-0.813853532979604, 51.56863013877188], [-0.81375715260415, 51.56839717647337], [-0.81371284208748, 51.56815934527622], [-0.813667666933597, 51.567788426746645], [-0.813915408556528, 51.567527470860746], [-0.814280993234198, 51.56684059466537], [-0.814704823511867, 51.56613272518902], [-0.814994772052415, 51.56569055892733], [-0.815202529336662, 51.56519091397622], [-0.815219131565213, 51.56505170869626], [-0.815351636137841, 51.564671793906264], [-0.815511116853932, 51.56436408558186], [-0.815565592175225, 51.5642108755608], [-0.815662011458114, 51.564053592855515], [-0.815731002538058, 51.56389693250399], [-0.815762954470268, 51.56377766396064], [-0.815882314039101, 51.56307031436626], [-0.815899206541573, 51.563030920866616], [-0.815918960262566, 51.5629924554096], [-0.816100387305838, 51.56272812828101], [-0.816239061970809, 51.562387838558415], [-0.816351227865814, 51.56217946080997], [-0.816402225233145, 51.562104443964415], [-0.816445731089239, 51.56204014171416], [-0.816755073070336, 51.56173843863565], [-0.81682883069147, 51.5615089920632], [-0.81709149295673, 51.56100450196165], [-0.817126546068515, 51.5610435199925], [-0.817036868291134, 51.56144185218239], [-0.817602599672938, 51.56148891511105], [-0.817650902097453, 51.56129517879352], [-0.81772376875861, 51.56109989153342], [-0.818544773098936, 51.56114952106405], [-0.818940645385275, 51.56112652991029], [-0.819286252234888, 51.56087194248433], [-0.820505192935939, 51.560762809232635], [-0.821152924392013, 51.56109751750433], [-0.821401734784996, 51.56112699167495], [-0.821468077854438, 51.561127657861135], [-0.821837057068471, 51.56108460493917], [-0.822551882107614, 51.56083281445182], [-0.822991148836609, 51.56069335159132], [-0.823298868990794, 51.56056425765945], [-0.823427756279142, 51.560489119484224], [-0.823816316952276, 51.56018999039105], [-0.823841530161624, 51.56016326766115], [-0.823879440321637, 51.5600917131651], [-0.823939663402364, 51.55926686943448], [-0.824000930577316, 51.55924050793579], [-0.823876052668265, 51.55798579953388], [-0.823797968341331, 51.557154174451156], [-0.823813910366727, 51.55698348995032], [-0.824268965579194, 51.556733581409404], [-0.824612742777356, 51.55649154853787], [-0.826344281900068, 51.55484359477631], [-0.826657025878971, 51.55457337099326], [-0.827233265990788, 51.554040521447654], [-0.827929376968602, 51.55344502444081], [-0.828029419518581, 51.553368693973354], [-0.828246668974642, 51.55322159923911], [-0.828485269800709, 51.5530855074047], [-0.828605105979095, 51.55302466009866], [-0.829372820563392, 51.55267264797847], [-0.830416538296143, 51.552300003050114], [-0.830341853307235, 51.55217517181714], [-0.830015969895102, 51.55217192349864], [-0.82991663595149, 51.55216463892005], [-0.829524156953837, 51.55211486704629], [-0.829282415323888, 51.552092673855796], [-0.828838545910614, 51.55207835426137], [-0.828599504055965, 51.55206338004898], [-0.828680519991051, 51.55199764928292], [-0.828534433277995, 51.55195752639793], [-0.828384829125967, 51.55188589688778], [-0.827987906418731, 51.55167242458396], [-0.827957477145366, 51.551621766572], [-0.827834232346298, 51.55159086272802], [-0.827633977763617, 51.551525919798664], [-0.827259642164726, 51.55144395104261], [-0.826879262755715, 51.5513727108443], [-0.826619197529414, 51.551334143723224], [-0.825020961294276, 51.55117248609027], [-0.825087548806697, 51.55099511486544], [-0.825080332863119, 51.55093929339289], [-0.825041927300218, 51.55086247849746], [-0.82502361605882, 51.55078946149474], [-0.825044884876699, 51.55069166368164], [-0.825058487270179, 51.550667521970404], [-0.82509857658956, 51.550622964257876], [-0.825145597538488, 51.55058926609727], [-0.825325151596322, 51.5505056412203], [-0.824909915146001, 51.550164291306395], [-0.82525013660448, 51.550002248032946], [-0.825332063085088, 51.549957209837], [-0.825638185418255, 51.54977504214384], [-0.82578350044032, 51.54967668704559], [-0.825923116734683, 51.54957557721289], [-0.826170603160388, 51.54937393870707], [-0.826371785812396, 51.54917813090106], [-0.826492161334072, 51.549039961540565], [-0.826611394860751, 51.548890091280356], [-0.82670116630089, 51.54876420424681], [-0.826782355507479, 51.54863553380353], [-0.826823019292071, 51.54856850170786], [-0.827062757600335, 51.548106920435835], [-0.827101368877001, 51.54800749719491], [-0.827194201898492, 51.54770629998806], [-0.827307057244965, 51.54741159704493], [-0.827582602426052, 51.54684696658141], [-0.827901482950928, 51.54639156924703], [-0.828226190833503, 51.54604592938406], [-0.828636291311094, 51.545744301711125], [-0.82879259021163, 51.54566673370386], [-0.828904643767125, 51.545627388779295], [-0.829074413403866, 51.54558682127211], [-0.829642313890905, 51.54554213206049], [-0.831137752395514, 51.5457125947128], [-0.831296230672639, 51.545718668970665], [-0.831381409559054, 51.545715021306385], [-0.831520416985355, 51.54569302674314], [-0.831658465956176, 51.54565213966273], [-0.832162223002951, 51.54540897973132], [-0.832259763552867, 51.545373083913695], [-0.832342495362879, 51.54535232675641], [-0.832449690786029, 51.545333611293344], [-0.832560958333776, 51.54532482723805], [-0.832748224427793, 51.54533298427906], [-0.833594396864807, 51.5455158387706], [-0.833803468208608, 51.5455736658201], [-0.834066591719905, 51.54566080332341], [-0.834319302360527, 51.54576042533909], [-0.834632701147966, 51.545911902800114], [-0.834743597643834, 51.54597414830218], [-0.834972095381443, 51.546118487532766], [-0.835101437527799, 51.546192605160904], [-0.835308853525451, 51.54642845092972], [-0.835597379030446, 51.546650714213946], [-0.835675530378703, 51.54669644865621], [-0.835765285050956, 51.54673960061442], [-0.835946924814448, 51.54679895002405], [-0.836738145066418, 51.54693268079673], [-0.837616543696299, 51.54698454508781], [-0.837668585980954, 51.546979665457], [-0.837867989708348, 51.546964555807335], [-0.83797367625272, 51.54694851789646], [-0.838105566972017, 51.54692284834119], [-0.838233315283634, 51.54688994418392], [-0.838329229403341, 51.54686122067977], [-0.838482151664417, 51.546802489195365], [-0.839036827411148, 51.54642762424692], [-0.83955396219585, 51.54605418383688], [-0.840116011657223, 51.54567219327131], [-0.840736658187885, 51.54525391246058], [-0.840941426195899, 51.54514083988849], [-0.841049279937165, 51.54509604683764], [-0.841194847094701, 51.5450426342828], [-0.841299520083949, 51.545009498946165], [-0.841898633062652, 51.544870644649805], [-0.842282244574123, 51.54481328536381], [-0.842762474116396, 51.54475597844143], [-0.843565178344648, 51.54477737891959], [-0.844086979548171, 51.54484366398717], [-0.845732620874625, 51.545006428046236], [-0.847066038668597, 51.54519847249473], [-0.847735465249349, 51.545415455459946], [-0.84773690700669, 51.54541546961599], [-0.848634731053663, 51.54572550693968], [-0.849946581441456, 51.54620234699594], [-0.850255922443757, 51.54628810259981], [-0.850450510421732, 51.5463493547082], [-0.850677807173489, 51.54642891042725], [-0.850994850981171, 51.546552505063595], [-0.852786328510159, 51.547342426670774], [-0.85300310746802, 51.547496506935786], [-0.853571212311598, 51.54778799706584], [-0.854020445083318, 51.54798930492918], [-0.854581807452525, 51.548205193183556], [-0.85478620907829, 51.54827822314386], [-0.854963577157503, 51.54833660211785], [-0.855161244465997, 51.54839068294659], [-0.855663052553423, 51.5485088736389], [-0.856166529832507, 51.54867563412252], [-0.856279761006, 51.54870371304979], [-0.856530960852844, 51.54875111981111], [-0.856804082121709, 51.54878705024175], [-0.857112033920981, 51.54881432744164], [-0.857381573472553, 51.548820548731904], [-0.857549123049589, 51.54881049056259], [-0.857844479857821, 51.548764809719025], [-0.858057604662229, 51.54872012637499], [-0.858337617310588, 51.54865361386837], [-0.858552497890933, 51.54859635820055], [-0.859049392399301, 51.548450127924276], [-0.859439117962245, 51.5483792842905], [-0.859697682244768, 51.54836291412634], [-0.859779823131423, 51.54836551056866], [-0.860170027263243, 51.548390881318824], [-0.860456238387038, 51.54842243448379], [-0.861009536104037, 51.548500639150554], [-0.861158816462501, 51.54852906321894], [-0.861352413335375, 51.54857320311727], [-0.861544411683264, 51.54862362144007], [-0.861731905414109, 51.5486811891874], [-0.862073439754406, 51.548807688741114], [-0.86254414698236, 51.54901726458452], [-0.86282747298222, 51.54916477803516], [-0.863015068128655, 51.549276295417606], [-0.863120154717426, 51.549342054318615], [-0.863285809311142, 51.54946594721784], [-0.863418211576468, 51.54959311456195], [-0.863530092020445, 51.54973357070672], [-0.863653360363745, 51.549937978681236], [-0.863733641061499, 51.550131180181424], [-0.863790336700716, 51.5503448343906], [-0.863824973966162, 51.550691353426465], [-0.863841717303786, 51.55100353042492], [-0.863889686614544, 51.55116225019172], [-0.863971345177536, 51.55135816235228], [-0.864192531443895, 51.5518009005184], [-0.864636908176204, 51.552606367995786], [-0.864885616784935, 51.55304487507487], [-0.864984710926909, 51.55317801261536], [-0.865042267578469, 51.5532415116859], [-0.86524661818949, 51.553434113390985], [-0.866448255146234, 51.55464702695421], [-0.866553639357719, 51.55475954277147], [-0.86713929523259, 51.555454866418074], [-0.86728119491555, 51.55560729781788], [-0.867357192432134, 51.55568356216347], [-0.867846797612144, 51.55612348793147], [-0.867948777192819, 51.5561991033314], [-0.868039688780355, 51.55625572917072], [-0.868162193200772, 51.55631805467487], [-0.868334859667638, 51.55639345221028], [-0.868507816648064, 51.556457162985154], [-0.868686743286876, 51.556512838449954], [-0.868885971532242, 51.55656421341042], [-0.869870431325262, 51.55700170522159], [-0.870538382015661, 51.557289577406756], [-0.87065953954758, 51.55734829060127], [-0.870968323528286, 51.55751940747054], [-0.871084688448939, 51.55759695683154], [-0.871173919806704, 51.55766345498963], [-0.871421843971099, 51.55790412108149], [-0.871517352960249, 51.558008444760674], [-0.871560414081151, 51.55807539786269], [-0.871689483441928, 51.558339198343305], [-0.871715457861831, 51.558455441841], [-0.871819115572425, 51.558871857901316], [-0.872116688986198, 51.559438501590606], [-0.87224925001588, 51.559678057158926], [-0.872395481580982, 51.5598898694294], [-0.872503199025109, 51.56002578071336], [-0.872616797176149, 51.56015725249727], [-0.872767427555836, 51.560307962534345], [-0.873712440787397, 51.560887112412736], [-0.874019840047307, 51.56105730856152], [-0.874413245272104, 51.56124990894107], [-0.875159636577155, 51.56157807024888], [-0.875469862748955, 51.561692540724444], [-0.875618818634067, 51.561736229028845], [-0.87605776549836, 51.56183754414599], [-0.876366373823489, 51.56190074371407], [-0.87668521195865, 51.561958645316366], [-0.876968369886447, 51.56200091867397], [-0.877197163756339, 51.56202468778881], [-0.877516822654725, 51.5620493254706], [-0.878098847659676, 51.56208186543848], [-0.879220205739232, 51.56212315048378], [-0.879742407250114, 51.562124539056946], [-0.8803196812009, 51.56211566057546], [-0.88047709073344, 51.562109069498774], [-0.880781923890852, 51.5620912956137], [-0.881058065229737, 51.56206695318683], [-0.881262135404653, 51.56204012483733], [-0.881418631531618, 51.562011943580096], [-0.881649213834679, 51.56196288783239], [-0.882003250428868, 51.56187993977645], [-0.882376388095226, 51.56178278572967], [-0.882764299497775, 51.56167138432022], [-0.883194585739291, 51.561537905452155], [-0.883498576630078, 51.5614364931922], [-0.883756281221724, 51.56134003501421], [-0.884086956514705, 51.56120920221847], [-0.884407665982054, 51.56107287871968], [-0.885457870533961, 51.5606602434964], [-0.885606584631741, 51.56059601575753], [-0.88578613479336, 51.56050960117672], [-0.88595858283258, 51.56041862303493], [-0.886091760097573, 51.560340759581955], [-0.886521253482028, 51.56006159461465], [-0.886883345687433, 51.559824949307504], [-0.887389733197074, 51.559468282123994], [-0.887580604245187, 51.55927227271282], [-0.887708691217493, 51.559107139161064], [-0.888191015763102, 51.55861266662589], [-0.888192457957821, 51.55861268028735], [-0.888287760756616, 51.55849039596854], [-0.888562664893145, 51.558278995716826], [-0.888677108662246, 51.55814070717499], [-0.888887527216288, 51.55791161122132], [-0.889030158653071, 51.55774121885465], [-0.889203011709727, 51.557514464273005], [-0.88945350380216, 51.55717964382524], [-0.889600043815338, 51.556967026485395], [-0.889736358446423, 51.55670036168757], [-0.889858423154084, 51.55648571413559], [-0.889957478394766, 51.55626815130874], [-0.88999480235023, 51.556157006507235], [-0.890171676314085, 51.5555274577222], [-0.89040150985715, 51.554855248874496], [-0.891496918227321, 51.55316795557777], [-0.892148150167392, 51.55212836271704], [-0.892302414411729, 51.5517746441604], [-0.892679622502675, 51.55097074336506], [-0.892968782461996, 51.55046633574481], [-0.893655961353766, 51.54930928124159], [-0.894173495307085, 51.548604708475054], [-0.894625223197592, 51.54805597035748], [-0.895275002004698, 51.547308578527485], [-0.895581553072133, 51.54691762432374], [-0.89567259455686, 51.54679079793778], [-0.895853734572665, 51.54651645501248], [-0.896746527292651, 51.54508077321264], [-0.896901839906062, 51.544860136456265], [-0.897255891587921, 51.54494888564188], [-0.898115394177492, 51.545187148067974], [-0.89822539056202, 51.54523044209882], [-0.898280925208036, 51.54525973711452], [-0.898317695099433, 51.54528975512979], [-0.898473190139101, 51.54530020648797], [-0.899619101095523, 51.54544313546729], [-0.901006352145532, 51.54562517973134], [-0.901825064903737, 51.54570208025918], [-0.903152296119515, 51.54586195719927], [-0.903215411109321, 51.54587603470979], [-0.903297183099556, 51.545893883241035], [-0.903017403675823, 51.54655396139701], [-0.902616538461231, 51.54745118838641], [-0.902395351374664, 51.54801290343283], [-0.902224993241029, 51.54837997226972], [-0.902166365216381, 51.54859882282856], [-0.902146545476892, 51.54876318664974], [-0.902208891983797, 51.54892921824649], [-0.902230894835185, 51.549033728477525], [-0.902380735585514, 51.54999904656443], [-0.902673692529055, 51.551610411429984], [-0.902806620567669, 51.55250003963136], [-0.902816651036337, 51.55268266601908], [-0.902804979597758, 51.55292803179951], [-0.902756997764027, 51.55318384821566], [-0.902654701329301, 51.55353896524768], [-0.902427419928527, 51.55423280167036], [-0.901847751389036, 51.555843197130926], [-0.901770133963284, 51.556011515784924], [-0.90172081219273, 51.556082988265544], [-0.902203201151337, 51.55617741874876], [-0.906010818638152, 51.55679743475821], [-0.906204734572883, 51.556771367660915], [-0.906311884484255, 51.55675438267928], [-0.906358975073264, 51.55689599188705], [-0.90638590166065, 51.55691602462513], [-0.906615524939952, 51.55696492104195], [-0.907278267614382, 51.55705831304524], [-0.907858939004531, 51.55720578727805], [-0.908174621569804, 51.5572752632291], [-0.908339760107985, 51.55730647205926], [-0.908420198492941, 51.557320707807335], [-0.908923514869315, 51.557385632424236], [-0.909322889103033, 51.5574540848968], [-0.909506497855653, 51.557497152980005], [-0.909658336207082, 51.557541723987356], [-0.909754316957776, 51.55756959081508], [-0.909871264277585, 51.557625526703404], [-0.91002439519459, 51.55767640345812], [-0.91014009429463, 51.557724234915874], [-0.910363938659539, 51.55783421414451], [-0.910769828656022, 51.55805288423369], [-0.910899633150579, 51.55817457810929], [-0.911134211672749, 51.55843931338367], [-0.911191431662737, 51.558519870694006], [-0.911262279817118, 51.55863382382822], [-0.911345597527786, 51.558829717406], [-0.911347039740573, 51.55882973078292], [-0.911354152248679, 51.55895478193117], [-0.911501156990394, 51.55932300828838], [-0.911744284152759, 51.55977395051585], [-0.911784714781588, 51.55983277167005], [-0.911848028397377, 51.559899897522016], [-0.911951124589094, 51.55999256899618], [-0.912043240814804, 51.56006176008463], [-0.912343203354435, 51.560247072388215], [-0.91252735254784, 51.560389049785144], [-0.912663001694362, 51.56050809825428], [-0.913644040185922, 51.561715780647766], [-0.914729932802775, 51.56149024267299], [-0.915936249212879, 51.561720788632314], [-0.917099342153636, 51.56255696600573], [-0.917716153850379, 51.56209778262442], [-0.918510926061236, 51.561494569835055], [-0.919190082061234, 51.56187308324347], [-0.919265061021478, 51.56187467281339], [-0.919319677161742, 51.561883268238525], [-0.919395565855925, 51.56190734542486], [-0.919450182060758, 51.561915940789575], [-0.919451624375034, 51.56191595406745], [-0.919612764956992, 51.561873377936905], [-0.919684473323871, 51.562134798108005], [-0.919731696029427, 51.56227280618616], [-0.919795125391412, 51.562396576626476], [-0.919877773921036, 51.56250074208832], [-0.920024454979703, 51.56264236275641], [-0.920273793481437, 51.562835281288635], [-0.920455811890565, 51.56300779833283], [-0.92054707457537, 51.56311384087594], [-0.920622684574115, 51.56321074767881], [-0.920768557091237, 51.56344767224896], [-0.920815933327546, 51.56357938704838], [-0.92087756775181, 51.56365728255545], [-0.920897207761338, 51.56368084160506], [-0.920950783915168, 51.5637334861553], [-0.921113636188415, 51.563862665653886], [-0.921195103215065, 51.56395602921301], [-0.921351882125165, 51.56422002842434], [-0.921395592904906, 51.56432383493418], [-0.921420893006769, 51.56441308549212], [-0.921438619701684, 51.56451755238376], [-0.921413985191466, 51.56464410933019], [-0.92167481147157, 51.56453500840272], [-0.923479752756133, 51.56373602685831], [-0.924155377789785, 51.5634113259626], [-0.924873111301507, 51.56399247344971], [-0.924951149378637, 51.56404803772923], [-0.925274524870032, 51.56428388466152], [-0.92664770764064, 51.56515336065692], [-0.927393574791524, 51.56558368874587], [-0.927927453464337, 51.5659491340051], [-0.92835633038721, 51.56599441223158], [-0.928244524844939, 51.56725223174753], [-0.928125342518282, 51.56741209518939], [-0.9281054698832, 51.56745956981058], [-0.927839528670842, 51.568092855254655], [-0.93052584537502, 51.568532778436946], [-0.930434864440691, 51.56872077542371], [-0.930290537945866, 51.568968530677175], [-0.93032458943141, 51.568993118606556], [-0.930407160146686, 51.56904062792432], [-0.930494248489543, 51.56908008580425], [-0.93056005729139, 51.5691040638391], [-0.930644619283805, 51.56912821268005], [-0.930755210497826, 51.569149901051155], [-0.931002865082897, 51.56917193845677], [-0.931506345671459, 51.5692367670135], [-0.9319179887186, 51.56928007686346], [-0.93222706378822, 51.569328746497334], [-0.932389335623021, 51.56936169347699], [-0.932508352034028, 51.56939334769875], [-0.932608510110525, 51.56942932617174], [-0.932871163025979, 51.56955040480943], [-0.933086053531851, 51.569616199317096], [-0.933414865356232, 51.569684826955594], [-0.93386895042082, 51.56976448240679], [-0.934635570791928, 51.5700537823344], [-0.934785550106084, 51.57011898456062], [-0.934985327167408, 51.57021431104797], [-0.935122135234023, 51.5702874858722], [-0.935333952219633, 51.570423383659445], [-0.935683954424458, 51.570697207984594], [-0.935948857561102, 51.57084617434456], [-0.936321009353978, 51.57109861741231], [-0.936776166293208, 51.57138058458356], [-0.936874033955222, 51.57139136186155], [-0.937185388877335, 51.57140497135624], [-0.937358713203379, 51.57139754881963], [-0.937474500211682, 51.571382411871916], [-0.937604964789291, 51.57135661760672], [-0.938082770631526, 51.571224267138255], [-0.938303744516349, 51.57115343316592], [-0.938622419406116, 51.57103852370375], [-0.938583754985448, 51.57127375671787], [-0.938615426417796, 51.5714008261012], [-0.938651384040882, 51.57152973256723], [-0.938810396393867, 51.572013125409214], [-0.938917102284257, 51.5723881447753], [-0.938977920287056, 51.572564931845086], [-0.939147081147104, 51.57298457478389], [-0.93921529881351, 51.57340150695511], [-0.939295227462625, 51.57374930879435], [-0.939359853061118, 51.57401065198471]]]}}
+ test/json-data/integers.json view
@@ -0,0 +1,1 @@+[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 6, 6, 7, 7, 7, 8, 9, 9, 10, 10, 11, 12, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 24, 26, 27, 29, 31, 33, 35, 37, 39, 42, 44, 47, 50, 53, 56, 60, 63, 67, 72, 76, 81, 86, 91, 96, 102, 109, 115, 122, 130, 138, 146, 155, 165, 175, 186, 197, 209, 222, 236, 250, 266, 282, 299, 318, 337, 358, 380, 403, 428, 454, 482, 511, 543, 576, 611, 649, 688, 730, 775, 823, 873, 926, 983, 1043, 1107, 1175, 1247, 1323, 1404, 1490, 1582, 1679, 1781, 1890, 2006, 2129, 2259, 2398, 2544, 2700, 2865, 3041, 3227, 3425, 3634, 3857, 4093, 4343, 4609, 4891, 5191, 5509, 5846, 6204, 6583, 6986, 7414, 7868, 8349, 8860, 9403, 9978, 10589, 11237, 11925, 12655, 13430, 14252, 15124, 16050, 17032, 18075, 19181, 20355, 21601, 22923, 24326, 25815, 27396, 29072, 30852, 32740, 34744, 36871, 39128, 41523, 44064, 46762, 49624, 52661, 55884, 59305, 62935, 66787, 70875, 75213, 79817, 84702, 89887, 95389, 101227, 107423, 113998, 120976, 128381, 136239, 144578, 153427, 162818, 172784, 183360, 194583, 206493, 219132, 232545, 246778, 261883, 277912, 294923, 312975, 332131, 352460, 374034, 396928, 421223, 447005, 474365, 503400, 534212, 566911, 601610, 638433, 677511, 718980, 762987, 809688, 859247, 911840, 967652, 1026880, 1089734, 1156434, 1227217, 1302333, 1382046, 1466638, 1556408, 1651673, 1752769, 1860052, 1973902, 2094721, 2222934, 2358996, 2503385, 2656613, 2819219, 2991777, 3174898, 3369227, 3575451, 3794297, 4026539, 4272995, 4534536, 4812086, 5106625, 5419191, 5750889, 6102889, 6476435, 6872845, 7293518, 7739939, 8213686, 8716429, 9249944, 9816115, 10416939, 11054539, 11731166, 12449207, 13211198, 14019829, 14877955, 15788605, 16754994, 17780533, 18868844, 20023768, 21249383, 22550016, 23930257, 25394980, 26949356, 28598872, 30349352, 32206975, 34178300, 36270285, 38490317, 40846232, 43346349, 45999492, 48815029, 51802899, 54973651, 58338478, 61909260, 65698602, 69719882, 73987297, 78515911, 83321713, 88421668, 93833782, 99577160, 105672079, 112140056, 119003924, 126287916, 134017747, 142220706, 150925751, 160163614, 169966908, 180370243, 191410345, 203126189, 215559137, 228753081, 242754599, 257613123, 273381107, 290114218, 307871529, 326715729, 346713346, 367934976, 390455540, 414354543, 439716356, 466630515, 495192035, 525501749, 557666661, 591800322, 628023236, 666463282, 707256166, 750545902, 796485316, 845236589, 896971830, 951873682, 1010135966, 1071964368, 1137577163, 1207205986, 1281096650, 1359510014, 1442722903, 1531029087, 1624740315, 1724187420, 1829721484, 1941715077, 2060563573, 2186686548, 2320529259, 2462564214, 2613292844, 2773247272, 2942992191, 3123126858, 3314287206, 3517148098, 3732425698, 3960880011, 4203317554, 4460594215, 4733618266, 5023353573, 5330822998, 5657112012, 6003372524, 6370826950, 6760772526, 7174585891, 7613727944, 8079749004, 8574294281, 9099109685, 9656047991, 10247075377, 10874278366, 11539871197, 12246203633, 12995769265, 13791214310, 14635346955, 15531147272, 16481777734, 17490594386, 18561158687, 19697250088, 20902879371, 22182302812, 23540037202, 24980875800, 26509905246, 28132523526, 29854459026, 31681790754, 33620969802, 35678842122, 37862672691, 40180171161, 42639519077, 45249398761, 48019023960, 50958172379, 54077220194, 57387178688, 60899733121, 64627283986, 68582990784, 72780818484, 77235586822, 81963022620, 86979815309, 92303675844, 97953399235, 103948930895, 110311437058, 117063379497, 124228594829, 131832378662, 139901574895, 148464670491, 157551896043, 167195332496, 177429024407, 188289100133, 199813899374, 212044108527, 225022904322, 238796106249, 253412338321, 268923200725, 285383451995, 302851202324, 321388118716, 341059642687, 361935221296, 384088552321, 407597844432, 432546093294, 459021374572, 487117154867, 516932621682, 548573033590, 582150091830, 617782334651, 655595555791, 695723248569, 738307077168, 783497376747, 831453684183, 882345301285, 936351892486, 993664119121, 1054484312524, 1119027188325, 1187520604468, 1260206365627, 1337341076854, 1419197049486, 1506063262491, 1598246382662, 1696071847252, 1799885012878, 1910052374747, 2026962860500, 2151029203266, 2282689398739, 2422408251457, 2570679015712, 2728025136906, 2895002099486, 3072199387991, 3260242568131, 3459795495242, 3671562657914, 3896291665080, 4134775885316, 4387857247705, 4656429214122, 4941439933460, 5243895588908, 5564863950114, 5905478142772, 6266940648935, 6650527552175, 7057593042589, 7489574197539, 7947996055022, 8434476997558, 8950734465625, 9498591020797, 10079980779998, 10696956243580, 11351695541337, 12046510122031, 12783852913581, 13566326982715, 14396694724673, 15277887615381, 16213016560543, 17205382878181, 18258489953389, 19376055606456, 20562025218016, 21820585657560, 23156180064488, 24573523533875, 26077619762337, 27673778712750, 29367635360200, 31165169585327, 33072727285306, 35097042776985, 37245262571278, 39524970602741, 41944215003394, 44511536515321, 47235998642351, 50127219647252, 53195406507421, 56451390948928, 59906667686130, 63573435001862, 67464637811456, 71594013362620, 75976139732519, 80626487293267, 85561473327514, 90798519986944, 96356115798305, 102253880934088, 108512636478302, 115154477931865, 122202853217119, 129682645456833, 137620260819954, 146043721744222, 154982765864743, 164468950997792, 174535766550465, 185218751749486, 196555621106568, 208586397563259, 221353553785311, 234902162105402, 249280053643550, 264537987166964, 280729828285480, 297912739615178, 316147382581544, 335498131574595, 356033301212013, 377825387512598, 400951323831468, 425492752460545, 451536312853150, 479173947490266, 508503226468250, 539627691953919, 572657223723034, 607708427072674, 644905044476938, 684378392439283, 726267825083707, 770721226121430, 817895530929871, 867957280587026, 921083209817197, 977460870923688, 1037289295911185, 1100779699135317, 1168156222959992, 1239656729054927, 1315533638126921, 1396054821049394, 1481504544536185, 1572184474698156, 1668414742025481, 1770535071555377, 1878905982215138, 1993910059574563, 2115953306501003, 2245466576485316, 2382907094698829, 2528760072151156, 2683540418647385, 2847794560591953]
+ test/json-data/jp10.json view
@@ -0,0 +1,1 @@+{"results":[{"from_user_id_str":"2458313","profile_image_url":"http://a2.twimg.com/profile_images/1203653060/fure091226_normal.png","created_at":"Thu, 27 Jan 2011 20:30:04 +0000","from_user":"19princess","id_str":"30724239224995840","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6d77\u6674\u300c\u672c\u65e5\u306e\u6771\u4eac\u306e\u304a\u5929\u6c17\u306f\u6674\u6642\u3005\u66c7\u3067\u3057\u3087\u3046\u3002\u6700\u9ad8\u6c17\u6e29\u306f8\u5ea6\u3001\u6700\u4f4e\u6c17\u6e29\u306f1\u5ea6\u3067\u3059\u3002\u3042\u306a\u305f\u306e\u4eca\u65e5\u306e\u4eba\u751f\u306b\u3068\u3073\u3063\u304d\u308a\u306e\u304a\u5929\u6c17\u3092\u2665\u300d","id":30724239224995840,"from_user_id":2458313,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://babyprincess.sakura.ne.jp/about/&quot; rel=&quot;nofollow&quot;&gt;\u5929\u4f7f\u5bb6\u306e\u88cf\u5c71&lt;/a&gt;"},{"from_user_id_str":"66578965","profile_image_url":"http://a0.twimg.com/profile_images/1183763267/fossetta_normal.png","created_at":"Thu, 27 Jan 2011 20:00:04 +0000","from_user":"Fossetta_Tokyo","id_str":"30716689779785729","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u304a\u306f\u3088\u3046\u3054\u3056\u3044\u307e\u3059!!\u6771\u4eac\u90fd\u3001\u672c\u65e5\u306e\u304a\u5929\u6c17\u306f\u6674\u6642\u3005\u66c7\u3002\u6700\u9ad8\u6c17\u6e298\u5ea6\u3001\u6700\u4f4e\u6c17\u6e291\u5ea6\u3002\u6771\u4eac\u5730\u65b9\u3067\u306f\u3001\u7a7a\u6c17\u306e\u4e7e\u71e5\u3057\u305f\u72b6\u614b\u304c\u7d9a\u3044\u3066\u3044\u307e\u3059\u3002\u706b\u306e\u53d6\u308a\u6271\u3044\u306b\u6ce8\u610f\u3057\u3066\u4e0b\u3055\u3044\u3002\u4f0a\u8c46\u8af8\u5cf6\u3068\u5c0f\u7b20\u539f\u8af8\u5cf6\u306b\u306f\u3001\u5f37\u98a8\u3001\u6ce2\u6d6a\u3001\u4e7e\u71e5\u3001\u971c\u306e\u6ce8\u610f\u5831\u3092\u767a\u8868\u4e2d\u3067\u3059\u3002","id":30716689779785729,"from_user_id":66578965,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://bit.ly/Fossetta&quot; rel=&quot;nofollow&quot;&gt;\u30d5\u30a9\u30bb\u30c3\u30bf ver.3.1.1&lt;/a&gt;"},{"from_user_id_str":"104041146","profile_image_url":"http://a2.twimg.com/profile_images/863965118/001jwatokyo_normal.jpg","created_at":"Thu, 27 Jan 2011 19:44:33 +0000","from_user":"jwa_tokyo","id_str":"30712787537756160","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6c17\u8c61\u5e81\u304b\u3089\u6771\u4eac\u306e\u5929\u6c17\u4e88\u5831\u304c\u767a\u8868\u3055\u308c\u307e\u3057\u305f\u3000\u305d\u306e\u5929\u6c17\u4e88\u5831\u3092\u3053\u3053\u3067\u3064\u3076\u3084\u3053\u3046\u304b\u306a\u3041\uff5e\uff1f\u3000\u8003\u3048\u4e2d\u3000\u307e\u3066\u6b21\u53f7\uff08\u7b11","id":30712787537756160,"from_user_id":104041146,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.docodemo.jp/twil/&quot; rel=&quot;nofollow&quot;&gt;Twil2 (Tweet Anytime, Anywhere by Mail)&lt;/a&gt;"},{"from_user_id_str":"144500192","profile_image_url":"http://a3.twimg.com/a/1294874399/images/default_profile_3_normal.png","created_at":"Thu, 27 Jan 2011 18:59:33 +0000","from_user":"kyo4to4","id_str":"30701462774358016","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6771\u4eac\u90fd \u516b\u4e08\u5cf6 - \u4eca\u65e5\u306e\u5929\u6c17\u306f\u30fb\u30fb\u30fb\u66c7\u306e\u3061\u6674\u3067\u3059\u306e\uff01","id":30701462774358016,"from_user_id":144500192,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/kyo4to4&quot; rel=&quot;nofollow&quot;&gt;lost-sheep-bot&lt;/a&gt;"},{"from_user_id_str":"165724582","profile_image_url":"http://a0.twimg.com/profile_images/1222842347/163087_1256281064506_1753997852_484926_2761052_n_normal.jpg","created_at":"Thu, 27 Jan 2011 17:39:06 +0000","from_user":"Rifqi_19931020","id_str":"30681213748387840","metadata":{"result_type":"recent"},"to_user_id":113796067,"text":"@CHLionRagbaby \u30b1\u30f3\u3061\u3083\u3093\u3001\u671d\u4eca\u307e\u3067\u306e\u81ea\u5206\u306e\u30db\u30fc\u30e0\u30a8\u30ea\u30a2\u304b\u3089\u307e\u3060\u975e\u5e38\u306b\u5bd2\u3044\u3068\u96e8\u304c\u964d\u3063\u3066\u3044\u305f..\u3069\u306e\u3088\u3046\u306b\u73fe\u5728\u306e\u6771\u4eac\u306e\u5929\u6c17\uff1f","id":30681213748387840,"from_user_id":165724582,"to_user":"CHLionRagbaby","geo":null,"iso_language_code":"ja","to_user_id_str":"113796067","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"120911929","profile_image_url":"http://a0.twimg.com/profile_images/772435076/20100312___capture2_normal.png","created_at":"Thu, 27 Jan 2011 17:28:30 +0000","from_user":"Cirno_fan","id_str":"30678546020040705","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u4eca\u65e5\u306e\u6771\u4eac\u306e\u5929\u6c17\u306f\u3001\u66c7\u6642\u3005\u6674\u3067\u6700\u9ad8\u6c17\u6e29\u306f8\u2103\uff01 \u6700\u4f4e\u6c17\u6e29\u306f2\u2103\u3060\u3063\u305f\u3088\uff01 RT @mimi22999 \uff08\uff65\u2200\uff65\uff09\uff4c\u3001\u865a\u5f31\u306a\u8005\u306b\u3068\u3063\u3066\u3001\u6717\u3089\u304b\u306a\u9854\u306f\u4e0a\u5929\u6c17\u3068\u540c\u3058\u304f\u3089\u3044\u3046\u308c\u3057\u3044\u3082\u306e\u3060\u3002\u30d5\u30e9\u30f3\u30af\u30ea\u30f3 #tenki #bot","id":30678546020040705,"from_user_id":120911929,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://blog.livedoor.jp/fairycirno/archives/34686.html&quot; rel=&quot;nofollow&quot;&gt;\u5e7b\u60f3\u90f7 \u9727\u306e\u6e56&lt;/a&gt;"},{"from_user_id_str":"65976527","profile_image_url":"http://a0.twimg.com/profile_images/452810990/little_italies_____normal.jpg","created_at":"Thu, 27 Jan 2011 17:00:03 +0000","from_user":"heta_weather01","id_str":"30671388754845696","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3010\u30d8\u30bf\u5929\u3011Tere hommikust.\u30a8\u30b9\u30c8\u30cb\u30a2\u3067\u3059\u3002\u4eca\u65e5\u306e\u6771\u4eac\u306e\u5929\u6c17\u306f\u6674\u6642\u3005\u66c7\u3067\u660e\u65e5\u306f\u66c7\u308a\u3067\u3059\u3002\u3061\u306a\u307f\u306b\u50d5\u306e\u3068\u3053\u308d\u3067\u306f\u66c7\u308a\u3067\u6c17\u6e29-7\u2103\u3067\u3059\u3002\u3061\u306a\u307f\u306b\u30b9\u30ab\u30a4\u30d7\u306e\u958b\u767a\u672c\u90e8\u306f\u50d5\u306e\u5bb6\u306b\u3042\u308b\u3093\u3067\u3059\u3002","id":30671388754845696,"from_user_id":65976527,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www15.atpages.jp/~kageyanma/&quot; rel=&quot;nofollow&quot;&gt;\u5730\u7403\u306e\u4e2d&lt;/a&gt;"},{"from_user_id_str":"7278433","profile_image_url":"http://a3.twimg.com/profile_images/1218965303/michael-1_normal.png","created_at":"Thu, 27 Jan 2011 16:50:16 +0000","from_user":"shimohiko","id_str":"30668926035689472","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3042\u308a\u3002\n\u3010\u7f8e\u4eba\u5929\u6c17/\u6771\u4eac\u3011\u7f8e\u4eba\u5929\u6c17\u30ad\u30e3\u30b9\u30bf\u30fc\u306e&quot;\u3055\u3042\u3084\u3093\u3055\u3093&quot;\u306b\u3088\u308b\u3068\u300c1/29(\u571f)\u306f\u304f\u3082\u308a\u3067\u3001\u964d\u6c34\u78ba\u738740%\u3001\u6700\u9ad8\u6c17\u6e29\u306f6\u2103\u3067\u6700\u4f4e\u6c17\u6e29\u306f1\u2103\u3067\u3059\u300d\u7f8e\u4eba\u5929\u6c17\u21d2http://bit.ly/djB8th http://twitpic.com/3twnl0 #bt_tenki","id":30668926035689472,"from_user_id":7278433,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://bijintenki.jp&quot; rel=&quot;nofollow&quot;&gt;bijintenki.jp&lt;/a&gt;"},{"from_user_id_str":"61770","profile_image_url":"http://a3.twimg.com/profile_images/1206955079/tw172a_normal.png","created_at":"Thu, 27 Jan 2011 16:40:52 +0000","from_user":"rsky","id_str":"30666561853333504","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3075\u3068\u591c\u7a7a\u3092\u898b\u4e0a\u3052\u3066\u30aa\u30ea\u30aa\u30f3\u304c\u898b\u3048\u306a\u304f\u3066\u300c\u6771\u4eac\u306b\u306f\u7a7a\u304c\u306a\u3044\u300d\u3068\u667a\u6075\u5b50\u306e\u3088\u3046\u306a\u3053\u3068\u3092\u601d\u3063\u305f\u308f\u3051\u3060\u304c\u3001\u305f\u3076\u3093\u534a\u5206\u3050\u3089\u3044\u306f\u5929\u6c17\u306e\u305b\u3044\u3067\u3059","id":30666561853333504,"from_user_id":61770,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.hootsuite.com&quot; rel=&quot;nofollow&quot;&gt;HootSuite&lt;/a&gt;"},{"from_user_id_str":"103259265","profile_image_url":"http://a0.twimg.com/profile_images/767857973/anime_icon_normal.gif","created_at":"Thu, 27 Jan 2011 16:37:42 +0000","from_user":"liveshowonly","id_str":"30665765698928640","metadata":{"result_type":"recent"},"to_user_id":null,"text":"iPhone\u5929\u6c17\u3002\u6771\u4eac4\u2103\u3063\u3066\u7d50\u69cb\u5bd2\u304f\u306a\u3044\u3058\u3083\u3093\u3002\uff08\u78ba\u304b\u306b\u3082\u306e\u51c4\u304f\u5bd2\u3044\u8a33\u3067\u306f\u7121\u3044\uff09\u3002\u798f\u5ca1\u5e02\u306f1\u2103\u3060\u3002\u52dd\u3061\u3060\u305c\u3002Rock'n'roll","id":30665765698928640,"from_user_id":103259265,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"165242761","profile_image_url":"http://a1.twimg.com/profile_images/1147334744/SN3E00600001_normal.jpg","created_at":"Thu, 27 Jan 2011 16:37:35 +0000","from_user":"deuxavril0502","id_str":"30665736556912640","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6b8b\u5ff5\u3001\u79c1\u306f\u6c96\u7e04\u3067\u3059(&gt;_&lt;)\u3067\u3082\u304a\u5929\u6c17\u826f\u3055\u305d\u3046\u3067\u826f\u304b\u3063\u305f\u306d\u3002\u3053\u3061\u3089\u306f\u96e8\u3088\uff5eRT @rinandy2010: \u305d\u3046\u3067\u3059\u3063\u266a\u51fa\u5f35\u3067(^^)\u304a\u306d\u3048\u3055\u307e\u306f\u6771\u4eac\u3067\u3059\u304b\uff1f\u3081\u3063\u3061\u3083\u5929\u6c17\u826f\u304f\u3066\u3073\u3063\u304f\u308a\u3067\u3059\u3002\u624b\u888b\u3068\u304b\u5168\u7136\u3044\u3089\u306a\u3044\u3067\u3059\u306d\u30fc RT @deuxavril0502 \u6771\u4eac\u306a\u306e\u30fc\uff1f","id":30665736556912640,"from_user_id":165242761,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.echofon.com/&quot; rel=&quot;nofollow&quot;&gt;Echofon&lt;/a&gt;"},{"from_user_id_str":"163900288","profile_image_url":"http://a1.twimg.com/profile_images/1210224823/icon12945064955238_normal.jpg","created_at":"Thu, 27 Jan 2011 16:24:37 +0000","from_user":"nyao_yurichan","id_str":"30662472734089216","metadata":{"result_type":"recent"},"to_user_id":100985873,"text":"@ray_ko302 \u767a\u898b\u3042\u308a\u304c\u3068\u3067\u3059\u3045\u3002\u3053\u3061\u3089\u3067\u3082\u3088\u308d\u3057\u304f\u306d\u3002\u6771\u4eac\u306f\u4eca\u65e5\u3082\u4e7e\u71e5\u3067\u5927\u5909\u3088\u3002\u305d\u3063\u3061\u3068\u771f\u9006\u306e\u5929\u6c17\u3060\u306d\u3002\u30a2\u30a4\u30b9\u30d0\u30fc\u30f3\u304d\u3092\u3064\u3051\u3066\u3088\u306d\u3002","id":30662472734089216,"from_user_id":163900288,"to_user":"ray_ko302","geo":null,"iso_language_code":"ja","to_user_id_str":"100985873","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"49227098","profile_image_url":"http://a3.twimg.com/profile_images/965007882/____normal.jpg","created_at":"Thu, 27 Jan 2011 15:52:26 +0000","from_user":"dosannko6","id_str":"30654371016478720","metadata":{"result_type":"recent"},"to_user_id":108570870,"text":"@peke_hajiP \u4ffa\u304c\u6771\u4eac \u6765\u3066\u6700\u521d\u306b\u9a5a\u3044\u305f\u306e\u304c\u3001\u5929\u6c17\u4e88\u5831\u3067\u82b1\u7c89\u60c5\u5831\u304c\u6d41\u308c\u308b\u3053\u3068\u3067\u3001\u6700\u521d\u306b\u8a66\u3057\u305f\u306e\u304c \u30b4\u30ad\u69d8 \u53ec\u559a\u306e\u5100\u5f0f\u3067\u3059\u304a\uff1f","id":30654371016478720,"from_user_id":49227098,"to_user":"peke_hajip","geo":null,"iso_language_code":"ja","to_user_id_str":"108570870","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"71148934","profile_image_url":"http://a0.twimg.com/profile_images/1209223630/image_normal.jpg","created_at":"Thu, 27 Jan 2011 15:46:49 +0000","from_user":"123keiko","id_str":"30652958311981058","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u9727\u5cf6\u306e\u964d\u7070\u306e\u30cb\u30e5\u30fc\u30b9\u3092\u898b\u3066\u601d\u3044\u51fa\u3057\u307e\u3057\u305f\u3002\u4e0a\u4eac\u3057\u305f\u59cb\u3081\u306e\u9803\u3001\u6771\u4eac\u306f1\u5e74\u4e2d\u3001\u7070\u304c\u964d\u3089\u306a\u3044\u304b\u3089\u7a7a\u6c17\u304c\u6f84\u3093\u3067\u3066\u904e\u3054\u3057\u3084\u3059\u3044\u306a\u3041\u3001\u3068\u601d\u3063\u305f\u306a\u3041\u3001\u3063\u3066\u3002\n\u5b9f\u5bb6\u306f\u51ac\u306e\u5b63\u7bc0\u98a8\u3067\u685c\u5cf6\u306e\u7070\u304c\u964d\u308b\u5730\u533a\u3067\u3057\u305f\u3002\u9e7f\u5150\u5cf6\u306e\u5929\u6c17\u4e88\u5831\u3067\u306f\u3001\u6bce\u65e5\u3001\u685c\u5cf6\u4e0a\u7a7a\u306e\u98a8\u5411\u304d\u4e88\u5831\u304c\u3067\u307e\u3059\u3002\u3053\u308c\u304b\u3089\u306f\u9727\u5cf6\u3082\u304b\u306a\uff1f","id":30652958311981058,"from_user_id":71148934,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"91124773","profile_image_url":"http://a3.twimg.com/profile_images/1139308356/prof101007_3-1_normal.jpg","created_at":"Thu, 27 Jan 2011 15:43:35 +0000","from_user":"sanposuruhito","id_str":"30652146277945345","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u4eca\u5bb5\u306e\u90fd\u5fc3\u306f\u3001\u8eab\u3082\u5f15\u304d\u7de0\u307e\u308b\u3068\u3044\u3046\u3088\u308a\u306f\u51cd\u3048\u308b\u7a0b\u306e\u51b7\u6c17\u3092\u611f\u3058\u308b\u5bd2\u3044\u591c\u3067\u3059\u3002\u660e\u65e5\u65e5\u4e2d\u306e\u6771\u4eac\u306e\u5929\u6c17\u306f\u3001\u5915\u65b9\u306b\u591a\u5c11\u96f2\u304c\u51fa\u308b\u5834\u6240\u304c\u3042\u308a\u305d\u3046\u306a\u3082\u306e\u306e\u6982\u306d\u6674\u308c\u7a7a\u304c\u5e83\u304c\u308a\u7d9a\u3051\u305d\u3046\u3067\u3059\u3002\u6700\u9ad8\u6c17\u6e29\u306f\u30015-6\u5ea6\u4f4d\u3068\u306a\u308a\u305d\u3046\u3067\u3059\u3002#weather_tokyo","id":30652146277945345,"from_user_id":91124773,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://projects.playwell.jp/go/Saezuri&quot; rel=&quot;nofollow&quot;&gt;Saezuri&lt;/a&gt;"}],"max_id":30724239224995840,"since_id":28179956995457024,"refresh_url":"?since_id=30724239224995840&q=%E6%9D%B1%E4%BA%AC%E3%80%80%E5%A4%A9%E6%B0%97","next_page":"?page=2&max_id=30724239224995840&lang=ja&q=%E6%9D%B1%E4%BA%AC%E3%80%80%E5%A4%A9%E6%B0%97","results_per_page":15,"page":1,"completed_in":0.104827,"warning":"adjusted since_id to 28179956995457024 (), requested since_id was older than allowed -- since_id removed for pagination.","since_id_str":"28179956995457024","max_id_str":"30724239224995840","query":"%E6%9D%B1%E4%BA%AC%E3%80%80%E5%A4%A9%E6%B0%97"}
+ test/json-data/jp100.json view
@@ -0,0 +1,1 @@+{"results":[{"from_user_id_str":"178045354","profile_image_url":"http://a0.twimg.com/profile_images/1214479381/154722_10100141954893059_808622_55276626_7445050_n_normal.jpg","created_at":"Fri, 25 Feb 2011 17:40:04 +0000","from_user":"ChuriSta_gt","id_str":"41190705623732224","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3072\u3089\u304c\u306a\u306e\u300c\u3064\u300d\u306f\u3082\u3046Twitter\u306e\u300c\u3064\u300d\u3060\uff01","id":41190705623732224,"from_user_id":178045354,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twipple.jp/&quot; rel=&quot;nofollow&quot;&gt;\u3064\u3044\u3063\u3077\u308b for iPhone&lt;/a&gt;"},{"from_user_id_str":"71746558","profile_image_url":"http://a2.twimg.com/profile_images/591641547/wwd_normal.gif","created_at":"Fri, 25 Feb 2011 17:40:02 +0000","from_user":"fuckkilldiestar","id_str":"41190699571494912","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u306b\u3057\u3066\u3082Twitter\u3084pixiv\u96e2\u308c\u3092\u3069\u3046\u306b\u304b\u3057\u308d\u674f\u4ec1\u30d6\u30eb\u30de","id":41190699571494912,"from_user_id":71746558,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"131230176","profile_image_url":"http://a2.twimg.com/sticky/default_profile_images/default_profile_5_normal.png","created_at":"Fri, 25 Feb 2011 17:39:58 +0000","from_user":"proory_bot","id_str":"41190683922546688","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u30ea\u30cd\u30f3\u30dd\u30fc\u30c1 http://bit.ly/btjqlH","id":41190683922546688,"from_user_id":131230176,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://proory.com/&quot; rel=&quot;nofollow&quot;&gt;proory tems&lt;/a&gt;"},{"from_user_id_str":"120359038","profile_image_url":"http://a1.twimg.com/profile_images/946521484/200805161826000_normal.jpg","created_at":"Fri, 25 Feb 2011 17:39:58 +0000","from_user":"tukinosuke","id_str":"41190680835391488","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @fuefukioyasumi: \u3082\u3046\u3044\u3044\u3002\u300c\u671d\u751f\u300d\u306a\u3093\u304b\u3044\u3044\u3002BBC\u3082CNN\u3082\u82f1\u8a9e\u653e\u9001\u3060\u3002\u307f\u3093\u306a\u3001\u30c6\u30ec\u30d3\u3092\u3076\u3061\u3063\u3068\u5207\u3063\u3066Twitter\u3092\u898b\u3088\u3046\u3002\u3053\u3053\u306b\u300c\u751f\u304d\u305f\u60c5\u5831\u300d\u304c\u6d41\u308c\u3066\u3044\u308b\u3002\u300c\u751f\u304d\u305f\u53eb\u3073\u300d\u304c\u3042\u308b\u3002\u3053\u3053\u304b\u3089\u3067\u3082\u6b74\u53f2\u306e\u8ee2\u63db\u70b9\u3092\u611f\u3058\u308b\u4e8b\u304c\u3067\u304d\u308b\u306e\u3060\u3002 @gjmorley #libjp","id":41190680835391488,"from_user_id":120359038,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"166512109","profile_image_url":"http://a3.twimg.com/profile_images/1224583217/image_normal.jpg","created_at":"Fri, 25 Feb 2011 17:39:50 +0000","from_user":"yurii1129","id_str":"41190649839620096","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u4eca\u6c17\u4ed8\u3044\u305f\u2026\u307f\u3043\u304f\u3093\u306eTwitter\u540d\u3001\u3084\u307e\u306d\u3084\u3093(\uffe3\u25c7\uffe3;)","id":41190649839620096,"from_user_id":166512109,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"228999619","profile_image_url":"http://a3.twimg.com/sticky/default_profile_images/default_profile_6_normal.png","created_at":"Fri, 25 Feb 2011 17:39:49 +0000","from_user":"takami926","id_str":"41190644793745408","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u7530\u539f\u3055\u3093\u3001\u5fdc\u63f4\u3057\u3066\u307e\u3059\u3002WEB\u3082TWITTER\u3082\u62dd\u898b\u3057\u3066\u307e\u3059\u3002\u7530\u539f\u3055\u3093\u304c\u653f\u6cbb\u5bb6\u306b\u306a\u3063\u3066\u3001\u65e5\u672c\u3092\u5909\u3048\u3066\u304f\u3060\u3055\u3044\u3002\u5c16\u95a3\u3001\u5317\u65b9\u554f\u984c\u5171\u306b\u65e5\u672c\u306e\u653f\u6cbb\u306e\u5931\u6557\u3067\u3059\u3002\u8a55\u8ad6\u5bb6\u306f\u8272\u3005\u8a00\u3063\u3066\u307e\u3059\u304c\u3001\u4e2d\u6771\u307b\u3069\u56fd\u6c11\u306e\u71b1\u6c17\u304c\u7121\u304f\u3001\u683c\u5dee\u304c\u3042\u308a\u3001\u4f55\u3082\u5909\u308f\u3089\u306a\u3044\u666f\u6c17\u4f4e\u8ff7\u306e\u65e5\u672c\u306b\u56fd\u6c11\u306f\u8ae6\u3081\u3066\u308b\u3093\u3067\u3059\u3002","id":41190644793745408,"from_user_id":228999619,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"151773701","profile_image_url":"http://a0.twimg.com/profile_images/1213699753/obi_nao_normal.jpg","created_at":"Fri, 25 Feb 2011 17:39:44 +0000","from_user":"obi_nao","id_str":"41190622949941248","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3060\u3044\u3076\u5e74\u4e0b\u306e\u5f8c\u8f29\u306b\u3001\u30d3\u30b8\u30cd\u30b9\u3084\u308b\u306a\u3089\u771f\u5263\u306bTwitter\u3084\u308c\uff01\u3063\u3066\u6012\u3089\u308c\u305f\u3002\u306a\u306e\u3067\u4eca\u65e5\u304b\u3089\u771f\u5263\u306b\u3064\u3076\u3084\u304f\u3053\u3068\u306b\u3057\u307e\u3059\u3002\u3088\u308d\u3057\u304f\u3067\u3059\u3002","id":41190622949941248,"from_user_id":151773701,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.flight.co.jp/iPhone/TweetMe/&quot; rel=&quot;nofollow&quot;&gt;TweetMe for iPhone&lt;/a&gt;"},{"from_user_id_str":"20817229","profile_image_url":"http://a0.twimg.com/profile_images/1212012721/205_normal.jpg","created_at":"Fri, 25 Feb 2011 17:39:43 +0000","from_user":"allte","id_str":"41190619804082176","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u7206\u7b11\u3057\u3066\u547c\u5438\u56f0\u96e3\u3067\u75d9\u6523\u3059\u308b\u5618\u304f\u3093\u304c\u898b\u308c\u308b\u306e\u306fTwitter\u3060\u3051","id":41190619804082176,"from_user_id":20817229,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.movatwi.jp&quot; rel=&quot;nofollow&quot;&gt;www.movatwi.jp&lt;/a&gt;"},{"from_user_id_str":"166907967","profile_image_url":"http://a2.twimg.com/profile_images/1250526230/___normal.jpg","created_at":"Fri, 25 Feb 2011 17:39:43 +0000","from_user":"NTTYAHOOOOOOOI","id_str":"41190618235412480","metadata":{"result_type":"recent"},"to_user_id":106469594,"text":"@takahirororo \u3066\u304b\u4f55\u3067twitter\u4e0a\u3067\u30a2\u30c9\u30d0\u30a4\u30b9\u3082\u3089\u3063\u3066\u3093\u306d\u3093\uff57","id":41190618235412480,"from_user_id":166907967,"to_user":"takahirororo","geo":null,"iso_language_code":"ja","to_user_id_str":"106469594","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"82581287","profile_image_url":"http://a0.twimg.com/profile_images/774721566/miyaru2_normal.jpg","created_at":"Fri, 25 Feb 2011 17:39:38 +0000","from_user":"kine_rahchaos","id_str":"41190598132244480","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u30a2\u30aa\u30b7\u30de\u3055\u3093\u304cTwitter\u59cb\u3081\u305f\u3060\u3068\u2026","id":41190598132244480,"from_user_id":82581287,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://sourceforge.jp/projects/tween/wiki/FrontPage&quot; rel=&quot;nofollow&quot;&gt;Tween&lt;/a&gt;"},{"from_user_id_str":"136633905","profile_image_url":"http://a2.twimg.com/profile_images/1247161991/yama_normal.png","created_at":"Fri, 25 Feb 2011 17:39:35 +0000","from_user":"yamachi39","id_str":"41190584706285568","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3084\u3063\u3071\u308amixi\u3088\u308atwitter\u306e\u304c\u597d\u304d\u304b\u3082","id":41190584706285568,"from_user_id":136633905,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://jigtwi.jp/?p=1&quot; rel=&quot;nofollow&quot;&gt;jigtwi&lt;/a&gt;"},{"from_user_id_str":"44029295","profile_image_url":"http://a2.twimg.com/profile_images/1112965522/image_normal.jpg","created_at":"Fri, 25 Feb 2011 17:39:32 +0000","from_user":"no2yosee","id_str":"41190572769280000","metadata":{"result_type":"recent"},"to_user_id":97721124,"text":"@hn0345 \u3042\u3001\u500b\u4eba\u7684\u306b\u306f\u4eca\u306ftwitter\u306e\u30d7\u30ed\u30d5\u30a3\u30fc\u30eb\u306b\u66f8\u3044\u3066\u3042\u308b\u30d6\u30ed\u30b0\u306e\u30e6\u30cb\u30c3\u30c8\u3092\u306e\u3093\u3073\u308a\u3084\u3063\u3066\u307e\u3059w","id":41190572769280000,"from_user_id":44029295,"to_user":"hn0345","geo":null,"iso_language_code":"ja","to_user_id_str":"97721124","source":"&lt;a href=&quot;http://www.flight.co.jp/iPhone/TweetMe/&quot; rel=&quot;nofollow&quot;&gt;TweetMe for iPhone&lt;/a&gt;"},{"from_user_id_str":"149872179","profile_image_url":"http://a0.twimg.com/profile_images/1145770150/Winter11_normal.jpg","created_at":"Fri, 25 Feb 2011 17:39:31 +0000","from_user":"xmyyyxx","id_str":"41190566951653376","metadata":{"result_type":"recent"},"to_user_id":95069405,"text":"@rindofu \u653b\u7565\u672c\u3042\u308c\u3070\u3044\u3044\u306e\u306b\u3063\u3066\u601d\u3044\u307e\u3059(T\u03c9T)/~~~ \u79c1\u3082\u76f4\u3057\u305f\u3044\u3068\u3053\u3044\u3063\u3071\u3044\u3060\u3051\u3069\u3001\u3042\u306860\u5e74\u304f\u3089\u3044\u3044\u304d\u3089\u308c\u308b\u306f\u305a\u3060\u3057\u3001\u306e\u3093\u3073\u308a\u76f4\u308c\u3070\u3044\u3044\u306a\u3042 \u304a\u3084\u3059\u307f\u3067\u3059^^\uff01\u308a\u3093\u3055\u3093\u3068twitter\u3067\u4ef2\u826f\u304f\u306a\u308c\u3066\u3088\u304b\u3063\u305f\u3067\u3059\u3063","id":41190566951653376,"from_user_id":149872179,"to_user":"rindofu","geo":null,"iso_language_code":"ja","to_user_id_str":"95069405","source":"&lt;a href=&quot;http://twtr.jp&quot; rel=&quot;nofollow&quot;&gt;Keitai Web&lt;/a&gt;"},{"from_user_id_str":"105579892","profile_image_url":"http://a0.twimg.com/profile_images/1212950455/love_happy_pink_normal.jpg","created_at":"Fri, 25 Feb 2011 17:39:30 +0000","from_user":"love_happy_pink","id_str":"41190566494601216","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u305d\u308c\u898b\u305f\u3044\u306a\u266aRT @kazukt \u3046\u308b\u304a\u307c\u7d75\u3001\u4ed6\u306e\u4eba\u306e\u3082\u898b\u3066\u3044\u305f\u3089\u3084\u305f\u3089\u30de\u30f3\u30ac\u306e\u30ad\u30e3\u30e9\u30af\u30bf\u30fc\u3067\u3080\u3061\u3083\u304f\u3061\u3083\u4e0a\u624b\u3044\u4eba\u304c\uff01\u2026\u3068\u3001\u30d7\u30ed\u30d5\u30a3\u30fc\u30eb\u898b\u305f\u3089\u3001\u306a\u3093\u3068\u3054\u672c\u4eba\u304c\u66f8\u3044\u3066\u3044\u3089\u3063\u3057\u3083\u3063\u305f\u3002Twitter\u3063\u3066\u30b9\u30b4\u30a4\u308f\u3002","id":41190566494601216,"from_user_id":105579892,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.nibirutech.com/&quot; rel=&quot;nofollow&quot;&gt;TwitBird iPad&lt;/a&gt;"},{"from_user_id_str":"16996368","profile_image_url":"http://a0.twimg.com/profile_images/1198892387/pochi_normal.jpg","created_at":"Fri, 25 Feb 2011 17:39:30 +0000","from_user":"HyoYoshikawa","id_str":"41190564187611136","metadata":{"result_type":"recent"},"to_user_id":1800565,"text":"@shinichiro_beck \u73fe\u5730\u304b\u3089\u306a\u3089\u826f\u304f\u3066\u4ed6\u306e\u5730\u57df\u304b\u3089\u3060\u4f59\u8a08\u306a\u3089\u3001\u4f55\u306e\u305f\u3081\u306bTwitter\u304c\u3042\u308b\u306e\u304b\u308f\u304b\u3089\u306a\u3044\u3088\u3002\u3044\u307e\u8d77\u304d\u3066\u308b\u3053\u3068\u306e\u8aac\u660e\u3082\u3064\u304b\u306a\u3044\u3002\u30a2\u30eb\u30b8\u30e3\u30b8\u30fc\u30e9\u304c\u4f1d\u3048\u3066\u308b\u3053\u3068\u304c\u5168\u3066\u3058\u3083\u306a\u3044\u3057\u3001\u65e5\u672c\u306eTV\u306f\u4f1d\u3048\u306a\u3044\u3067\u3057\u3087\u3046\u3002\u77e5\u308a\u305f\u304c\u3063\u3066\u308b\u4eba\u3082\u305f\u304f\u3055\u3093\u3044\u308b\u3002","id":41190564187611136,"from_user_id":16996368,"to_user":"shinichiro_beck","geo":null,"iso_language_code":"ja","to_user_id_str":"1800565","source":"&lt;a href=&quot;http://www.hootsuite.com&quot; rel=&quot;nofollow&quot;&gt;HootSuite&lt;/a&gt;"},{"from_user_id_str":"119070802","profile_image_url":"http://a2.twimg.com/profile_images/1229773005/346d8435099798113a1326e1ba4949ee_normal.jpeg","created_at":"Fri, 25 Feb 2011 17:39:25 +0000","from_user":"takotako726","id_str":"41190545439207424","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u9b54\u6cd5\u5c11\u5973\u307e\u3069\u304b\u30de\u30ae\u30ab\u306e\u53cd\u97ff\u304ctwitter\u3067\u306f\u3093\u3071\u306d\u3048\u4ef6\u3002","id":41190545439207424,"from_user_id":119070802,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://z.twipple.jp/&quot; rel=&quot;nofollow&quot;&gt;\u3064\u3044\u3063\u3077\u308b/twipple&lt;/a&gt;"},{"from_user_id_str":"165695316","profile_image_url":"http://a1.twimg.com/profile_images/681748782/ring_normal.jpg","created_at":"Fri, 25 Feb 2011 17:39:21 +0000","from_user":"pandora_shotbar","id_str":"41190526447390720","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u300e\u30b7\u30e7\u30c3\u30c8\u30d0\u30fc\u3000\u30d1\u30f3\u30c9\u30e9\u3000\u30c4\u30a4\u30c3\u30bf\u30fc\u306e\u3064\u3076\u3084\u304d\u300f\u30b7\u30e7\u30c3\u30c8\u30d0\u30fc\u3000\u30d1\u30f3\u30c9\u30e9\u3000\u30c4\u30a4\u30c3\u30bf\u30fc\u306e\u3064\u2026\uff5chttp://pandora-twitter.seesaa.net/article/187809936.html","id":41190526447390720,"from_user_id":165695316,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://blog.seesaa.jp/&quot; rel=&quot;nofollow&quot;&gt;SeesaaBlog&lt;/a&gt;"},{"from_user_id_str":"88862311","profile_image_url":"http://a2.twimg.com/profile_images/1182243698/161113_100001897885617_169366_q_normal.jpg","created_at":"Fri, 25 Feb 2011 17:39:16 +0000","from_user":"amebaguide","id_str":"41190504926416896","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u300e\u30a2\u30e1\u30d6\u30ed\u653b\u7565\u30ac\u30a4\u30c9\u306e\u30c4\u30a4\u30c3\u30bf\u30fc\u306e\u3064\u3076\u3084\u304d\u300f\u30d6\u30ed\u30b0\u306e\u653b\u7565\u3000\u30c4\u30a4\u30c3\u30bf\u30fc\u306e\u3064\u3076\u3084\u304d\u96c6\uff5chttp://accessup-twitter.seesaa.net/article/187809931.html","id":41190504926416896,"from_user_id":88862311,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://blog.seesaa.jp/&quot; rel=&quot;nofollow&quot;&gt;SeesaaBlog&lt;/a&gt;"},{"from_user_id_str":"186895007","profile_image_url":"http://a2.twimg.com/profile_images/1229610799/image_normal.jpg","created_at":"Fri, 25 Feb 2011 17:39:11 +0000","from_user":"zizikt","id_str":"41190486123356161","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3053\u308c\u306f\u500b\u4eba\u7684\u306b\u306f\u91cd\u5b9d\u3067\u3059\u3002\n\u25c6iPhone&amp;iPad\u5411\u3051Twitter\u30a2\u30d7\u30ea\u300c\u3064\u3044\u3063\u3077\u308b\u300d\u306bEvernote\u3068\u306e\u9023\u643a\u6a5f\u80fd\u3092\u8ffd\u52a0 \nhttp://bit.ly/hPf44u","id":41190486123356161,"from_user_id":186895007,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"132320738","profile_image_url":"http://a3.twimg.com/profile_images/1229760475/image_normal.jpg","created_at":"Fri, 25 Feb 2011 17:39:04 +0000","from_user":"faina_","id_str":"41190453592334336","metadata":{"result_type":"recent"},"to_user_id":167894537,"text":"@gjmorley \u30e2\u30fc\u30ea\u30fc\u3055\u3093\u306e\u7ffb\u8a33\u306e\u8a00\u8449\u306e\u30bb\u30f3\u30b9\u304c\u3059\u304d\u3067\u3059\u3002\u305d\u3057\u3066\u3001Twitter\u3067\u77e5\u308b\u3001\u73fe\u5730\u306e\u4eba\u306e\u76ee\u306e\u529b\u3084\u3001\u60c5\u5831\u3092\u4f1d\u3048\u3066\u304f\u308c\u308b\u7686\u3055\u3093\u306e\u60c5\u71b1\u306b\u611f\u52d5\u3057\u3066\u3044\u307e\u3059\u3002\u611f\u52d5\u3059\u308b\u3063\u3066\u3059\u3054\u3044\u30a8\u30cd\u30eb\u30ae\u30fc\u3067\u3059\uff01\uff01","id":41190453592334336,"from_user_id":132320738,"to_user":"gjmorley","geo":null,"iso_language_code":"ja","to_user_id_str":"167894537","source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"24870552","profile_image_url":"http://a3.twimg.com/profile_images/907130634/4_normal.jpg","created_at":"Fri, 25 Feb 2011 17:39:03 +0000","from_user":"tana1192","id_str":"41190450480021504","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u500b\u4eba\u7684\u306b\u306f\u3001\u30bf\u30b0\u5f3e\u304d\u3059\u308c\u3070\u3044\u3044\uff08\u5f3e\u3051\u308b\u30c4\u30fc\u30eb\u3092\u4f7f\u3063\u3066Twitter\u3092\u3059\u308c\u3070\u3044\u3044\uff09\u3068\u601d\u3046\u3093\u3060\u3051\u3069\u306a\u3042\u3002\u307f\u3093\u306a\u305d\u308c\u306a\u308a\u306bTwitter\u3084\u308b\u4eba\u3060\u3057\u3001\u30c4\u30fc\u30eb\u9078\u3073\u304f\u3089\u3044\u5e45\u3092\u6301\u3063\u3066\u3084\u3063\u3066\u3082\u3002 \u307e\u3042\u3001\u4ffa\u30a2\u30cb\u30e1\u306f\u89b3\u308b\u89b3\u308b\u8a50\u6b3a\u3059\u308b\u3060\u3051\u306e\u4eba\u3060\u304b\u3089\u95a2\u4fc2\u306a\u3044\u3093\u3060\u3051\u3069\u3082\uff57\uff57\u5b9f\u6cc1\u306f\u697d\u3057\u305d\u3046\u306b\u898b\u3048\u308b\u3088\u3002","id":41190450480021504,"from_user_id":24870552,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://sourceforge.jp/projects/tween/wiki/FrontPage&quot; rel=&quot;nofollow&quot;&gt;Tween&lt;/a&gt;"},{"from_user_id_str":"58509646","profile_image_url":"http://a3.twimg.com/sticky/default_profile_images/default_profile_2_normal.png","created_at":"Fri, 25 Feb 2011 17:39:02 +0000","from_user":"kakuyas","id_str":"41190446663344128","metadata":{"result_type":"recent"},"to_user_id":107076877,"text":"@Dayspool \u6df1\u591c\uff12\uff19\u6642\u304f\u3089\u3044\u307e\u3067\u306f\u6bce\u65e5\u55b6\u696d\u3057\u3066\u308b\u306e\u3067\u3001twitter\u3001\u30e1\u30c3\u30bb\u3001PS3\u306a\u3069\u3067\u3088\u3093\u3067\u3051\u308c\u3002\u3053\u3063\u3061\u304b\u3089\u58f0\u304b\u3051\u308b\u3053\u3068\u3082\u3042\u308b\u304b\u3082\u3060\u304c\uff57","id":41190446663344128,"from_user_id":58509646,"to_user":"Dayspool","geo":null,"iso_language_code":"ja","to_user_id_str":"107076877","source":"&lt;a href=&quot;http://cheebow.info/chemt/archives/2007/04/twitterwindowst.html&quot; rel=&quot;nofollow&quot;&gt;Twit for Windows&lt;/a&gt;"},{"from_user_id_str":"149128527","profile_image_url":"http://a2.twimg.com/profile_images/1254922702/image_normal.jpg","created_at":"Fri, 25 Feb 2011 17:39:00 +0000","from_user":"uni13yoki","id_str":"41190437087748096","metadata":{"result_type":"recent"},"to_user_id":119468594,"text":"@kuro_wr84 \u304a\u308c\u3068\u304a\u524d\u3067Twitter\u3067\u4f1a\u8a71\u3057\u305f\u3089\u3001\u5927\u5909\u306a\u3053\u3068\u306b\u306a\u308b\u306a","id":41190437087748096,"from_user_id":149128527,"to_user":"kuro_wr84","geo":null,"iso_language_code":"ja","to_user_id_str":"119468594","source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"18670595","profile_image_url":"http://a2.twimg.com/profile_images/1250674062/ProfilePhoto_normal.png","created_at":"Fri, 25 Feb 2011 17:38:59 +0000","from_user":"nyuuuuun","id_str":"41190436450222080","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3078\u30fc\u3053\u3093\u306a\u306b\u3088\u304f\u4f3c\u308b\u3053\u3068\u3082\u3042\u308b\u3093\u3060\u30fc\nhttp://twitter.com/Re_44/status/35943233016168448\nhttp://twitter.com/3510_misaka/status/36122114821988352","id":41190436450222080,"from_user_id":18670595,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"96267603","profile_image_url":"http://a3.twimg.com/profile_images/1245253473/DSiLL3_normal.jpg","created_at":"Fri, 25 Feb 2011 17:38:59 +0000","from_user":"arivis","id_str":"41190433119940608","metadata":{"result_type":"recent"},"to_user_id":null,"text":"http://twitter.com/#!/arivis/status/40636755380142080 \u306a\u3093\u3064\u3046\u304b\u3053\u308c\u3067\u8d64\u3063\u3066\u306e\u3082\u3061\u3087\u3063\u3068\u3042\u308c\u3063\u3059\u306d\u3002\u305d\u3057\u3066\u4e88\u671f\u3057\u3066\u3044\u305f\u304b\u306e\u3088\u3046\u3060","id":41190433119940608,"from_user_id":96267603,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"148097776","profile_image_url":"http://a3.twimg.com/profile_images/1166904064/broken-heart_normal.png","created_at":"Fri, 25 Feb 2011 17:38:56 +0000","from_user":"ReajuBreaker_A","id_str":"41190422135058432","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u672c\u57a2\u30d5\u30a9\u30ed\u30fc\u3088\u308d\u3057\u304f\u3067\u3059\u2192 http://twitter.com/ReajuBreaker \uff08\uff20\u3060\u3068\u30ea\u30d7\u30e9\u30a4\u304c\u57cb\u307e\u308b\u305f\u3081\u3001URL\u306b\u3057\u3066\u3042\u308a\u307e\u3059\uff09","id":41190422135058432,"from_user_id":148097776,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.twitter.com/ReajuBreaker&quot; rel=&quot;nofollow&quot;&gt;\u73fe\u5145\u6bba\u3057\u306e\u7121\u4eba\u9023\u545f\uff1c\u30aa\u30fc\u30c8\u30c4\u30a4\u30fc\u30c8\uff1e&lt;/a&gt;"},{"from_user_id_str":"59711654","profile_image_url":"http://a2.twimg.com/profile_images/416212928/flowers-06-1_normal.jpg","created_at":"Fri, 25 Feb 2011 17:38:56 +0000","from_user":"agamo45","id_str":"41190421522546688","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @ebizo66: \u30ea\u30d3\u30a2\u30fb\u30c8\u30ea\u30dd\u30ea\u306eTw\uff1a\u6551\u6025\u8eca\u304c\u6765\u305f\u304c\u3001\u8ca0\u50b7\u8005\u3092\u8eca\u5185\u3067\u6bba\u5bb3\u3057\u3066\u3044\u305f\u3001\u3068\u3002\uff08\u6ec5\u8336\u82e6\u8336\u3060\uff01\uff09http://ow.ly/43pQ2 #libjp","id":41190421522546688,"from_user_id":59711654,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.hootsuite.com&quot; rel=&quot;nofollow&quot;&gt;HootSuite&lt;/a&gt;"},{"from_user_id_str":"145311373","profile_image_url":"http://a3.twimg.com/profile_images/1187802576/a_gam_02_normal.png","created_at":"Fri, 25 Feb 2011 17:38:55 +0000","from_user":"nogic1008","id_str":"41190418141937664","metadata":{"result_type":"recent"},"to_user_id":88117317,"text":"@runasoru \u307e\u3042\u5acc\u306a\u3089\u6df1\u591c\u5e2f\u306bTwitter\u898b\u306a\u304d\u3083\u3044\u3044\u3060\u3051\u3067\u3059\u304b\u3089\u306d\u30fc \u30103/19BDM\u30aa\u30d5http://twvt.us/bdoff_kansai\u3011","id":41190418141937664,"from_user_id":145311373,"to_user":"runasoru","geo":null,"iso_language_code":"ja","to_user_id_str":"88117317","source":"&lt;a href=&quot;http://sourceforge.jp/projects/tween/wiki/FrontPage&quot; rel=&quot;nofollow&quot;&gt;Tween&lt;/a&gt;"},{"from_user_id_str":"195851300","profile_image_url":"http://a2.twimg.com/profile_images/1124572815/be7b4c0a06cb60e8_normal.jpg","created_at":"Fri, 25 Feb 2011 17:38:49 +0000","from_user":"LoveLAscrewDoll","id_str":"41190391147540480","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @jp_mjp: SCREW TV SHOW Vol.6\u306e\u653e\u9001\u304c\u7121\u4e8b\u306b\u7d42\u4e86\u3044\u305f\u3057\u307e\u3057\u305f\u3002\u3054\u89a7\u9802\u304d\u3042\u308a\u304c\u3068\u3046\u3054\u3056\u3044\u307e\u3057\u305f\uff01\u6b21\u56de\u653e\u9001\u306f3\u670818\u65e5(\u91d1)\u3067\u3059\u3002\u8996\u8074\u8005\u30d7\u30ec\u30bc\u30f3\u30c8\u306e\u8a73\u7d30\u306f\u5f8c\u65e5MJP\u3067\u304a\u77e5\u3089\u305b\u3044\u305f\u3057\u307e\u3059\u3002\u305d\u306e\u969b\u306b\u306f\u3001\u3053\u306eTwitter\u3067\u3082\u304a\u77e5\u3089\u305b\u3044\u305f\u3057\u307e\u3059\u306e\u3067\u3001\u304a\u697d\u3057\u307f\u306b\uff01#SCREWTV","id":41190391147540480,"from_user_id":195851300,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://z.twipple.jp/&quot; rel=&quot;nofollow&quot;&gt;\u3064\u3044\u3063\u3077\u308b/twipple&lt;/a&gt;"},{"from_user_id_str":"178045354","profile_image_url":"http://a0.twimg.com/profile_images/1214479381/154722_10100141954893059_808622_55276626_7445050_n_normal.jpg","created_at":"Fri, 25 Feb 2011 17:38:48 +0000","from_user":"ChuriSta_gt","id_str":"41190389549510656","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3053\u3053\u4e00\u9031\u9593Twitter\u304c\u3084\u305f\u3089\u697d\u3057\u3044\u306e\u3067\u3059\u3051\u3069\u30fc\u30fc\u30fc\uff01\u306b\u3072\u3072\u3063\u7b11","id":41190389549510656,"from_user_id":178045354,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twipple.jp/&quot; rel=&quot;nofollow&quot;&gt;\u3064\u3044\u3063\u3077\u308b for iPhone&lt;/a&gt;"},{"from_user_id_str":"50592111","profile_image_url":"http://a0.twimg.com/profile_images/1252176582/lFjpi_normal.jpg","created_at":"Fri, 25 Feb 2011 17:38:48 +0000","from_user":"crowNeko","id_str":"41190387271864320","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u30a8\u30ed\u30b2\u30d7\u30ec\u30a4\u3057\u306a\u304c\u3089\u307e\u3069\u30de\u30aeTL\u3092\u898b\u306a\u3044\u3088\u3046\u306b\u534a\u76ee\u306b\u306a\u308a\u306a\u304c\u3089Twitter\u3057\u3066\u308b\u6df1\u591c2\u6642\u534a\u3002","id":41190387271864320,"from_user_id":50592111,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://stone.com/Twittelator&quot; rel=&quot;nofollow&quot;&gt;Twittelator&lt;/a&gt;"},{"from_user_id_str":"7141467","profile_image_url":"http://a3.twimg.com/profile_images/1198540741/icon12912579345276_normal.jpg","created_at":"Fri, 25 Feb 2011 17:38:40 +0000","from_user":"hina_geshi","id_str":"41190355961380864","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u7d50\u5c40\u30cd\u30bf\u30d0\u30ec\u3059\u3093\u306a\u3063\u3066\u8a00\u3046\u65b9\u304c\u30a2\u30ec\u3060\u3088\u306d\u3001\u3057\u306a\u3044\u308f\u3051\u7121\u3044\u3058\u3083\u306a\u3044\u8133\u5185\u5782\u308c\u6d41\u3057\u304cTwitter\u306a\u3093\u3060\u304b\u3089\u3055","id":41190355961380864,"from_user_id":7141467,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://projects.playwell.jp/go/Saezuri&quot; rel=&quot;nofollow&quot;&gt;Saezuri&lt;/a&gt;"},{"from_user_id_str":"149608984","profile_image_url":"http://a0.twimg.com/profile_images/1205950885/090702_normal.jpg","created_at":"Fri, 25 Feb 2011 17:38:39 +0000","from_user":"Mr_Tsubaki","id_str":"41190349447774208","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u307e\u3069\u304b\u306fTwitter\u5408\u308f\u305b\u3066\uff11\u6642\u9593\u306f\u697d\u3057\u3044","id":41190349447774208,"from_user_id":149608984,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://z.twipple.jp/&quot; rel=&quot;nofollow&quot;&gt;\u3064\u3044\u3063\u3077\u308b/twipple&lt;/a&gt;"},{"from_user_id_str":"122271550","profile_image_url":"http://a0.twimg.com/profile_images/961936499/neoneko_bot5jpg_____normal.jpg","created_at":"Fri, 25 Feb 2011 17:38:39 +0000","from_user":"neoneko_bot5","id_str":"41190348726345728","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3010\u30c6\u30b9\u30c8\uff1a23\u3011\u4eca\u65e5\u304b\u3089\u5ba3\u4f1d\u958b\u59cb\u2606\u306d\u304a\u306d\u3053\u306eTwitter \u3067 EasyBottex\u3000\u3067\u304d\u308b\u307e\u3067\uff01\u2192http://neoneko.blog31.fc2.com/","id":41190348726345728,"from_user_id":122271550,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www20.atpages.jp/neoneko/&quot; rel=&quot;nofollow&quot;&gt;neoneko_bot5&lt;/a&gt;"},{"from_user_id_str":"127069188","profile_image_url":"http://a3.twimg.com/sticky/default_profile_images/default_profile_6_normal.png","created_at":"Fri, 25 Feb 2011 17:38:37 +0000","from_user":"MYCHEBOT","id_str":"41190340908163074","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u30eb\u30d5\u3055\u3093\u304c\u914d\u4fe1\u3092\u7d42\u4e86\u3057\u307e\u3057\u305f\uff01/ http://777labo.com/mychecker/view/745.php / \u30c8\u30d4\u30c3\u30af:XSplit\u30c6\u30b9\u30c8\u3000twitter\u2192http://twitter.com/rukh01","id":41190340908163074,"from_user_id":127069188,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://777labo.com/mychecker/&quot; rel=&quot;nofollow&quot;&gt;MYCHEBOT&lt;/a&gt;"},{"from_user_id_str":"107626142","profile_image_url":"http://a1.twimg.com/profile_images/1104968421/SuperMaika1_normal.jpg","created_at":"Fri, 25 Feb 2011 17:38:34 +0000","from_user":"MaikaPaPa","id_str":"41190328614666240","metadata":{"result_type":"recent"},"to_user_id":null,"text":"75\u5e74\u524d\u306e\u4eca\u65e52\u670826\u65e5\u672a\u660e\u3001\u82e5\u304d\u9752\u5e74\u5c06\u6821\u3089\u304c\u653f\u6cbb\u8150\u6557\u3068\u8fb2\u6751\u306e\u56f0\u7aae\u306b\u5bfe\u3057\u3066\u6c7a\u8d77\u3057\u307e\u3057\u305f\u3002\u3082\u3061\u308d\u3093\u5f53\u6642\u306fTwitter\u3082FB\u3082\u3001\u307e\u305f\u5f53\u7136\u306a\u304c\u3089Internet\u3082\u3042\u308a\u307e\u305b\u3093\u3067\u3057\u305f\u3002\u4e00\u65b9\u3001\u30c1\u30e5\u30cb\u30b8\u30a2\u3001\u30a8\u30b8\u30d7\u30c8\u3001\u4e2d\u6771\u306b\u5e83\u304c\u308b\u53cd\u653f\u5e9c\u30c7\u30e2\u3068\u5f37\u6a29\u4f53\u5236\u306e\u5d29\u58ca\u3002\u73fe\u5728\u306e\u65e5\u672c\u3067\u306f\u8003\u3048\u3089\u308c\u306a\u3044\u3053\u3068\u3067\u3059\u3002","id":41190328614666240,"from_user_id":107626142,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"11111695","profile_image_url":"http://a1.twimg.com/sticky/default_profile_images/default_profile_0_normal.png","created_at":"Fri, 25 Feb 2011 17:38:28 +0000","from_user":"kumatchipooh","id_str":"41190306145775617","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @inosenaoki: \u306a\u308b\u307b\u3069\u306d\u3002 RT @dansrmz @inosenaoki \u3053\u3061\u3089\u304c\u30bd\u30d5\u30c8\u30d0\u30f3\u30af\u526f\u793e\u9577\u306e\u677e\u672c\u5fb9\u4e09\u3055\u3093\u306e\u3001Twitter\u306b\u95a2\u3059\u308b\u8ad6\u8003\u3067\u3059\u3002 \u25b6 &quot;Twitter\u306e2\u30c1\u30e3\u30f3\u30cd\u30eb\u5316\u306f\u9632\u6b62\u51fa\u6765\u308b\u304b&quot; http://t.co/Mwzb8Zf","id":41190306145775617,"from_user_id":11111695,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"212161909","profile_image_url":"http://a1.twimg.com/profile_images/1252895790/natm01_normal.gif","created_at":"Fri, 25 Feb 2011 17:38:28 +0000","from_user":"n34hitman","id_str":"41190303830511616","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Twitter\u30fb\u30d6\u30ed\u30b0\u30a2\u30d5\u30a3\u30ea\u30a8\u30a4\u30c8\u81ea\u52d5\u6295.... http://goo.gl/a4RHB 4054","id":41190303830511616,"from_user_id":212161909,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twibow.net/&quot; rel=&quot;nofollow&quot;&gt;Twibow&lt;/a&gt;"},{"from_user_id_str":"105945593","profile_image_url":"http://a2.twimg.com/sticky/default_profile_images/default_profile_5_normal.png","created_at":"Fri, 25 Feb 2011 17:38:24 +0000","from_user":"nakano_bot","id_str":"41190285710983168","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3053\u306ebot\u306f\u4e0d\u52d5\u7523\u4f1a\u793e\u69d8\u5411\u3051\u306e\u4e0d\u52d5\u7523\u30dd\u30fc\u30bf\u30eb\u30b5\u30a4\u30c8\u3078\u306e\u4e00\u62ec\u8ee2\u9001\uff0bTwitter\u7121\u6599\u8ee2\u9001\u30b5\u30fc\u30d3\u30b9\u306b\u3088\u308a\u3064\u3076\u3084\u304d\u307e\u3059\u3002\u3054\u5229\u7528\u306b\u306a\u308a\u305f\u3044\u5834\u5408\u306f http://bit.ly/demey0 \u306b\u304a\u6c17\u8efd\u306b\u304a\u554f\u3044\u5408\u308f\u305b\u4e0b\u3055\u3044\u3002","id":41190285710983168,"from_user_id":105945593,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.teramax.jp/aboutus.html&quot; rel=&quot;nofollow&quot;&gt;tmxbot&lt;/a&gt;"},{"from_user_id_str":"170904226","profile_image_url":"http://a2.twimg.com/profile_images/1249020397/1789kb_normal.jpg","created_at":"Fri, 25 Feb 2011 17:38:21 +0000","from_user":"1789kb","id_str":"41190274428305408","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3068\u308a\u3042\u3048\u305a\u4eca\u56de\u521d\u3081\u3066\u30ea\u30a2\u30eb\u30bf\u30a4\u30e0\u3067\u898b\u3066\u308f\u304b\u3063\u305f\u306e\u306f\u3001QB\u306f\u5168\u8eab\u304c\u30a2\u30f3\u30d1\u30f3\u30bf\u30a4\u30d7\u3060\u3068\u3044\u3046\u3053\u3068\u3068\u3001\u307e\u3069\u30de\u30ae\u7d42\u4e86\u5f8c\u306eTwitter\u306e\u3056\u308f\u3064\u304d\u304c\u3059\u3054\u3044\u3068\u3044\u3046\u3053\u3068","id":41190274428305408,"from_user_id":170904226,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.echofon.com/&quot; rel=&quot;nofollow&quot;&gt;Echofon&lt;/a&gt;"},{"from_user_id_str":"166169644","profile_image_url":"http://a1.twimg.com/profile_images/1217017679/image_normal.jpg","created_at":"Fri, 25 Feb 2011 17:38:18 +0000","from_user":"Okkkkkkkkkkun","id_str":"41190262420017152","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3063\u3066Twitter\u3067\u3064\u3076\u3084\u304f\u81ea\u5206\u3082\u76f8\u5f53\u5c0f\u3055\u3044\u306a\u3002","id":41190262420017152,"from_user_id":166169644,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"196623456","profile_image_url":"http://a3.twimg.com/profile_images/1219076661/image_normal.jpg","created_at":"Fri, 25 Feb 2011 17:38:06 +0000","from_user":"NoMoneyClub","id_str":"41190213342601216","metadata":{"result_type":"recent"},"to_user_id":195462431,"text":"@yosal15 \nTwitter\u3084\u3063\u3066\u308b\u3089\u3057\u3044\u3002\u3051\u3069\u3001\u898b\u3064\u304b\u3089\u306a\u3044\u3002","id":41190213342601216,"from_user_id":196623456,"to_user":"yosal15","geo":null,"iso_language_code":"ja","to_user_id_str":"195462431","source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"126984048","profile_image_url":"http://a3.twimg.com/profile_images/1254579571/zipyaru-20090829-23-0012_normal.png","created_at":"Fri, 25 Feb 2011 17:38:02 +0000","from_user":"minaduki_naduki","id_str":"41190196418584576","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u203b\u30e1\u30fc\u30eb\u3067\u5973\u306e\u5b50\u53e3\u8aac\u304f\u306e\u306b\u5fd9\u3057\u3044\u3093\u3060\u305d\u3046\u3067\u3059\u3000\u307e\u3058\u3058\u3054\u308d RT @yowano_k: \u5fd9\u3057\u304f\u3066\u3082Twitter\u306b\u9854\u3092\u51fa\u3057\u305f\u304f\u306a\u308b\u50d5\u30a7\u2026\u2026","id":41190196418584576,"from_user_id":126984048,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://seesmic.com/seesmic_desktop/sd2&quot; rel=&quot;nofollow&quot;&gt;Seesmic Desktop&lt;/a&gt;"},{"from_user_id_str":"200568705","profile_image_url":"http://a3.twimg.com/profile_images/1242179765/101230_1355_01_normal.jpg","created_at":"Fri, 25 Feb 2011 17:38:01 +0000","from_user":"hammerstrap","id_str":"41190190428979200","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6628\u65e5\u91d1\u66dc\u65e5\u306f\u732e\u8840\u306b\u5f80\u304f\u3002\u4eca\u306e\u50d5\u306e\u4eba\u9593\u3068\u3057\u3066\u306e\u4fa1\u5024\u306f\u3001\u7cbe\u3005\u3053\u306e\u7a0b\u5ea6\u3002\u8179\u304c\u6e1b\u3063\u305f\u3002\u5374\u8aac\u3002twitter\u306e\u767e\u56db\u5341\u6587\u5b57\u306b\u82db\u3005\u3068\u3057\u3066\u3001\u4e00\u3064\u306e\u4eee\u8aac\u306b\u4fe1\u6191\u6027\u3092\u5f97\u308b\u3002\u6226\u6642\u4e0b\u306e\u7d19\u306e\u7d71\u5236\u3068\u3001\u4e2d\u5cf6\u6566\u306e\u6587\u7ae0\u306e\u95a2\u4fc2\u6027\u3002\u73fe\u5728\u30a6\u30a7\u30d6\u30ed\u30b0\u306b\u3001\u7e8f\u3081\u3066\u3044\u308b\u6700\u4e2d\u3002\u8fd1\u65e5\u516c\u958b\u4e88\u5b9a\u3002","id":41190190428979200,"from_user_id":200568705,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"164970427","profile_image_url":"http://a0.twimg.com/profile_images/1227176662/9192b021ef85ce9902c28955f86e604b_normal.jpg","created_at":"Fri, 25 Feb 2011 17:37:59 +0000","from_user":"syuigetsuIT","id_str":"41190184041197569","metadata":{"result_type":"recent"},"to_user_id":141926331,"text":"@iliad_tga \u30a4\u30ea\u30a2\u30b9\u3055\u3093\u304c\u305d\u306e\u8fba\u7406\u89e3\u306e\u3042\u308b\u3053\u3068\u306f\u5206\u304b\u3063\u3066\u307e\u3059\uff57\u3000\u30cd\u30bf\u30d0\u30ec\u4e91\u3005\u306ftwitter\u306e\u6c7a\u5b9a\u7684\u306a\u6b20\u70b9\u3060\u3068\u601d\u3044\u307e\u3059\u306d\u3047\u3002","id":41190184041197569,"from_user_id":164970427,"to_user":"iliad_tga","geo":null,"iso_language_code":"ja","to_user_id_str":"141926331","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"94526795","profile_image_url":"http://a0.twimg.com/profile_images/679411954/akb48_in_normal.jpg","created_at":"Fri, 25 Feb 2011 17:37:56 +0000","from_user":"akb48_in","id_str":"41190171248431104","metadata":{"result_type":"recent"},"to_user_id":null,"text":"AKB48\u67cf\u6728\u7531\u7d00 \u304a\u75b2\u308c\u69d8\u3002: \n\u3053\u3093\u3070\u3093\u306f(^O^)\uff0f\u266a\n\u3000\n\u3000\n\u3000\n\u4eca\u65e5\u306e\u516c\u6f14\u697d\u3057\u304b\u3063\u305f\u301c\n\u3000\n\u3000\n\u4e45\u3057\u3076\u308a\u3067\u7dca\u5f35\u3057\u305f\u3051\u3069\u3001\u306f\u3058\u3051\u307e\u304f\u3063\u305f\u305c\u3043\uff01\uff01\n\u3000\n\u3000\n\u3000\nMC\u306f\u565b\u307f\u307e\u304f\u308a\u306e\u30c6\u30f3\u30d1\u308a\u307e\u304f... http://bit.ly/ie8bEa akb48 twitter","id":41190171248431104,"from_user_id":94526795,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"222270374","profile_image_url":"http://a3.twimg.com/profile_images/1250946143/P1040062_normal.jpg","created_at":"Fri, 25 Feb 2011 17:37:53 +0000","from_user":"chrxpac","id_str":"41190158455943168","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3010\u5b9a\u671f\u3011Twitter\u306f\uff8a\uff9f\uff7f\uff7a\uff9d\u304b\u3089\u3057\u304b\u3067\u304d\u306a\u3044\u306e\u3067\uff98\uff8c\uff9f\u8fd4\u3057\u304c\u9045\u304f\u306a\u308a\u307e\u3059(\u00b4\u30fb\u03c9\u30fb\uff40)\u3054\u4e86\u627f\u4e0b\u3055\u3044!!","id":41190158455943168,"from_user_id":222270374,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twittbot.net/&quot; rel=&quot;nofollow&quot;&gt;twittbot.net&lt;/a&gt;"},{"from_user_id_str":"64009258","profile_image_url":"http://a2.twimg.com/profile_images/1154025023/Mixi___normal.jpg","created_at":"Fri, 25 Feb 2011 17:37:52 +0000","from_user":"Tatsuyuko","id_str":"41190153447944192","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u4f01\u696d\u304c\u5c31\u6d3b\u751f\u306bFacebook\u3092\u4f7f\u3063\u3066\u7533\u8acb\u305b\u3088\u3068\u3044\u3044\u3001\u4f01\u696d\u304cFacebook\u3092\u4f7f\u3048\u3068\u4f01\u696d\u304c\u8a00\u3063\u3066\u304d\u305f\u308a\u3059\u308c\u3070\u305d\u308c\u3069\u3053\u308d\u3058\u3083\u306a\u3044\u304b\u3068\u3002\u5c11\u306a\u304f\u3068\u3082\u5c31\u6d3b\u3067Twitter\u306e\u8a00\u52d5\u3084\u30d5\u30a9\u30ed\u30ef\u30fc\u3092\u8abf\u67fb\u3059\u308b\u4f01\u696d\u306f\u304b\u306a\u308a\u591a\u3044\u3067\u3059 RT @Isshee: \u4f01\u696d\u3067\u3082\u540c\u3058\u3067\u3057\u3087 RT","id":41190153447944192,"from_user_id":64009258,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://sourceforge.jp/projects/tween/wiki/FrontPage&quot; rel=&quot;nofollow&quot;&gt;Tween&lt;/a&gt;"},{"from_user_id_str":"105145175","profile_image_url":"http://static.twitter.com/images/default_profile_normal.png","created_at":"Fri, 25 Feb 2011 17:37:49 +0000","from_user":"snao813","id_str":"41190141703753728","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @yumisaiki: \u795e\u69d8\u306f\u672c\u5f53\u306b\u4eba\u9593\u3092\u3059\u3070\u3089\u3057\u3044\u30d0\u30e9\u30f3\u30b9\u3067\u914d\u7f6e\u3057\u3066\u304a\u3089\u308c\u308b\u3002\u795d\u5cf6\u307f\u305f\u3044\u306a\u5c0f\u3055\u3044\u3068\u3053\u308d\u306b\u306a\u3093\u3067\u3053\u3093\u306a\u306b\u7acb\u6d3e\u306a\u4eba\u304c\u305f\u304f\u3055\u3093\u3044\u308b\u3093\u3060\u308d\u3046\u3002\u795e\u69d8\u3042\u308a\u304c\u3068\u3046\u3002\u672c\u5f53\u306b\u3042\u308a\u304c\u3068\u3046\u3002\u4eba\u9593\u3063\u3066\u3059\u3070\u3089\u3057\u3044\u3068\u601d\u3048\u305f\u3002twitter\u3082\u3042\u308a\u304c\u3068\u3046\u3002\u3000#kaminoseki","id":41190141703753728,"from_user_id":105145175,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://seesmic.com/seesmic_desktop/sd2&quot; rel=&quot;nofollow&quot;&gt;Seesmic Desktop&lt;/a&gt;"},{"from_user_id_str":"203667204","profile_image_url":"http://a2.twimg.com/profile_images/1254664000/eve_blackcat_normal.jpg","created_at":"Fri, 25 Feb 2011 17:37:46 +0000","from_user":"eve_blackcat","id_str":"41190129292943361","metadata":{"result_type":"recent"},"to_user_id":146894340,"text":"@kuroiso02 \u3057\u308a\u3068\u308a\u3068\u304b\uff1f\n\u2026\u3067\u3082Twitter\u3060\u3068\u7d42\u308f\u3089\u306a\u304f\u306a\u308b\u30b1\u30c9\u2026","id":41190129292943361,"from_user_id":203667204,"to_user":"kuroiso02","geo":null,"iso_language_code":"ja","to_user_id_str":"146894340","source":"&lt;a href=&quot;http://twipple.jp/&quot; rel=&quot;nofollow&quot;&gt;\u3064\u3044\u3063\u3077\u308b for iPhone&lt;/a&gt;"},{"from_user_id_str":"6528403","profile_image_url":"http://a0.twimg.com/profile_images/1240591244/image_normal.jpg","created_at":"Fri, 25 Feb 2011 17:37:45 +0000","from_user":"sortiee","id_str":"41190125924917248","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u30a2\u30cb\u30e1\u5b9f\u6cc1\u306ftwitter\u306e\u764c","id":41190125924917248,"from_user_id":6528403,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://sites.google.com/site/peraperaprv/Home&quot; rel=&quot;nofollow&quot;&gt;P3:PeraPeraPrv&lt;/a&gt;"},{"from_user_id_str":"186058304","profile_image_url":"http://a0.twimg.com/profile_images/1250454564/IMG_0210_normal.JPG","created_at":"Fri, 25 Feb 2011 17:37:37 +0000","from_user":"00o8o00","id_str":"41190090386448384","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @fuefukioyasumi: \u3082\u3046\u3044\u3044\u3002\u300c\u671d\u751f\u300d\u306a\u3093\u304b\u3044\u3044\u3002BBC\u3082CNN\u3082\u82f1\u8a9e\u653e\u9001\u3060\u3002\u307f\u3093\u306a\u3001\u30c6\u30ec\u30d3\u3092\u3076\u3061\u3063\u3068\u5207\u3063\u3066Twitter\u3092\u898b\u3088\u3046\u3002\u3053\u3053\u306b\u300c\u751f\u304d\u305f\u60c5\u5831\u300d\u304c\u6d41\u308c\u3066\u3044\u308b\u3002\u300c\u751f\u304d\u305f\u53eb\u3073\u300d\u304c\u3042\u308b\u3002\u3053\u3053\u304b\u3089\u3067\u3082\u6b74\u53f2\u306e\u8ee2\u63db\u70b9\u3092\u611f\u3058\u308b\u4e8b\u304c\u3067\u304d\u308b\u306e\u3060\u3002 @gjmorley #libjp","id":41190090386448384,"from_user_id":186058304,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"49219631","profile_image_url":"http://a2.twimg.com/profile_images/1231873117/__2-colornow__2__normal.jpg","created_at":"Fri, 25 Feb 2011 17:37:33 +0000","from_user":"irisgazer","id_str":"41190073227550720","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3067\u3059\u3088\u306d\u3047 RT @minamitaiheiyou: \u6545\u306b\u30cd\u30bf\u30d0\u30ec\u3092\u3059\u308b\u306e\u3082\u81ea\u7531\u3002\u30cd\u30bf\u3070\u308c\u3057\u305f\u4eba\u3092\u30c7\u30a3\u30b9\u308b\u306e\u3082\u81ea\u7531\u3002\u3042\u3068\u306f\u5404\u3005\u306e\u88c1\u91cf\u3067\u4e57\u308a\u5207\u3063\u3066\u304f\u3060\u3055\u3044\u3088\u3081\u3093\u3069\u304f\u3055\u3044\u306a RT @kihirokiro: Twitter\u3067\u30cd\u30bf\u30d0\u30ec\u4e91\u3005\u3044\u3063\u3066\u3082\u5143\u3005Twitter\u3063\u3066\u300c\u500b\u4eba\u306e\u72ec\u308a\u8a00\u300d\u3060\u308d","id":41190073227550720,"from_user_id":49219631,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://sourceforge.jp/projects/tween/wiki/FrontPage&quot; rel=&quot;nofollow&quot;&gt;Tween&lt;/a&gt;"},{"from_user_id_str":"98560114","profile_image_url":"http://a0.twimg.com/profile_images/1252332545/IMGP8170_normal.jpg","created_at":"Fri, 25 Feb 2011 17:37:32 +0000","from_user":"qess0093","id_str":"41190070706905088","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u30b7\u30e5\u30fc\u30c6\u30a3\u304f\u305d\u3046\u305c\u3048\uff57 RT @syu_thi_bot: \u3044\u3064\u307e\u3067Twitter\u3084\u3063\u3066\u308b\u3093\u3060\u3044\uff1f\u3044\u3044\u52a0\u6e1b\u73fe\u5b9f\u306b\u623b\u308a\u306a\u3088\u3002\u3053\u3053\u306f\u30ad\u30df\u305f\u3061\u4e09\u6b21\u5143\u306e\u4eba\u9593\u304c\u3044\u308b\u3079\u304d\u5834\u6240\u3058\u3083\u306a\u3044\u3093\u3060\u3088\u3002\u305d\u3093\u306a\u306e\u57fa\u672c\u3060\u308d\uff01\uff01","id":41190070706905088,"from_user_id":98560114,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://sourceforge.jp/projects/tween/wiki/FrontPage&quot; rel=&quot;nofollow&quot;&gt;Tween&lt;/a&gt;"},{"from_user_id_str":"197459334","profile_image_url":"http://a3.twimg.com/profile_images/1210698414/shiratorishoko_normal.png","created_at":"Fri, 25 Feb 2011 17:37:31 +0000","from_user":"SyokoShiratori","id_str":"41190064243478528","metadata":{"result_type":"recent"},"to_user_id":null,"text":"+0.50kg \u540c\u3058\u304f\u30c0\u30a4\u30a8\u30c3\u30c8\u4e2d\u3067\u3059\u3002\u3088\u304f\u3053\u306e\u30b5\u30a4\u30c8\u3067\u4f53\u91cd\u5831\u544a\u3057\u3066\u307e\u3059\u266a  http://bit.ly/fhN2fM RT @msophiah \u3046\u3046\u3046\u30fb\u30fb\u30fb\u304a\u8179\u3059\u3044\u305f\u301c\u30fb\u30fb\u30fb\u6211\u6162\u3001\u6211\u6162\u3001\u6211\u6162\u3002\u30c0\u30a4\u30a8\u30c3\u30c8\u3001\u30c0\u30a4\u30a8\u30c3\u30c8\u3001\u30c0\u30a4\u30a8\u30c3\u30c8\u3002","id":41190064243478528,"from_user_id":197459334,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://mob-mc.com&quot; rel=&quot;nofollow&quot;&gt;\uff08\u4eee\u79f0\uff09\u30c4\u30a4\u30af\u30ea\u30c3\u30af twiclick&lt;/a&gt;"},{"from_user_id_str":"180410237","profile_image_url":"http://a3.twimg.com/profile_images/1254473979/icon_normal.jpg","created_at":"Fri, 25 Feb 2011 17:37:30 +0000","from_user":"ka_ph","id_str":"41190062184079360","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3010\u5b9a\u671fpost\u3011\u30d5\u30a7\u30eb\u30c7\u30a3\u30ca\u30f3\u30c8bot( http://twitter.com/ferdinand_bot )\u4f5c\u308a\u307e\u3057\u305f\u3002\u304a\u5b50\u69d8\u306e\u540d\u524d\u304a\u501f\u308a\u3055\u305b\u3066\u304f\u308c\u308b\u65b9\u3044\u307e\u3057\u305f\u3089@ka_ph\u307e\u3067\u304a\u9858\u3044\u3057\u307e\u3059\u3002","id":41190062184079360,"from_user_id":180410237,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twittbot.net/&quot; rel=&quot;nofollow&quot;&gt;twittbot.net&lt;/a&gt;"},{"from_user_id_str":"52283906","profile_image_url":"http://a3.twimg.com/profile_images/387644016/10100654519_s_normal.jpg","created_at":"Fri, 25 Feb 2011 17:37:26 +0000","from_user":"rolling_bean","id_str":"41190043313913856","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3081\u307e\u3044\u304c\u3057\u307e\u3059\u306d\u301cRT @yurikalin:\u30b3\u30a4\u30c4\u30e9\u5168\u54e1\u3044\u306a\u304f\u306a\u3063\u305f\u3089\u3001\u660e\u308b\u304f\u306a\u308b\u3060\u308d\u3046\u306a\u3041\uff5e\u266a\uff1e\u65e5\u672c\u306e\u30d3\u30b8\u30e7\u30f3 RT roll \u65e5\u7d4c\uff06CSIS\u30b7\u30f3\u30dd\u30b8\u30a6\u30e0\u300c\u5b89\u4fdd\u6539\u5b9a50\u5468\u5e74\u3001\u3069\u3046\u306a\u308b\u65e5\u7c73\u95a2\u4fc2\u300d http://bit.ly/dES8PY\u3000http://bit.ly/ihg0GT","id":41190043313913856,"from_user_id":52283906,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://tabtter.jp&quot; rel=&quot;nofollow&quot;&gt;\u30bf\u30d6\u30c3\u30bf\u30fc&lt;/a&gt;"},{"from_user_id_str":"351896","profile_image_url":"http://a3.twimg.com/profile_images/1179865336/icon12911887173020_normal.jpg","created_at":"Fri, 25 Feb 2011 17:37:26 +0000","from_user":"cicada","id_str":"41190043292925952","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u4e2d\u5b66\u53d7\u9a13\u7a0b\u5ea6\u306e\u7b97\u6570\u306e\u554f\u984c\u3092\u51fa\u3059BOT http://twitter.com/arithmetic_bot  \u6c17\u306b\u306f\u306a\u308b\u304c\u3001\u3061\u3083\u3093\u3068\u898b\u308b\u65e5\u304c\u6765\u308b\u3060\u308d\u3046\u304b\u30fb\u30fb\u3000#tearai\uff1a\u30ed\u30b3\u30e9\u30dc\u5bae\u5d0e\u770c http://locolabo.com/mz/ #mzlf","id":41190043292925952,"from_user_id":351896,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://locolabo.com/mz/&quot; rel=&quot;nofollow&quot;&gt;\u30ed\u30b3\u30e9\u30dc\u5bae\u5d0e\u770c&lt;/a&gt;"},{"from_user_id_str":"744554","profile_image_url":"http://a1.twimg.com/profile_images/1088753125/11868894_normal.gif","created_at":"Fri, 25 Feb 2011 17:37:20 +0000","from_user":"Febreze","id_str":"41190019020505088","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u30cd\u30bf\u30d0\u30ec\u3042\u307e\u308a\u6c17\u306b\u3057\u306a\u3044\u30bf\u30a4\u30d7\u3060\u3051\u3069\u306a\u3093\u304b\u3053\u3046Twitter\u958b\u304f\u3060\u3051\u3067\u30ac\u30f3\u30ac\u30f3\u30cd\u30bf\u30d0\u30ec\u5165\u3063\u3066\u304f\u308b\u306e\u306f\u306a\u3093\u3068\u3082\u8a00\u3048\u306a\u3044\u306a\u3002","id":41190019020505088,"from_user_id":744554,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"203371794","profile_image_url":"http://a1.twimg.com/profile_images/1226537594/___normal.png","created_at":"Fri, 25 Feb 2011 17:37:16 +0000","from_user":"himeko24","id_str":"41190004139114496","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6b21\u3044\u3063\u3066\u307f\u3088\u30fc\u3000\u306f\u3044\u3053\u308c\u3000 \u6fc0\u5b89\u26052010\u5e74\u590f\u65b0\u30c7\u30b6\u30a4\u30f3\u2605\u30bb\u30af\u30b7\u30fc\u306a\u9023\u4f53\u5f0f\u7121\u5730\u6c34\u7740 \u80f8\u30d1\u30c3\u30c9\u4ed8\u304dQ122 http://bit.ly/h72C4i","id":41190004139114496,"from_user_id":203371794,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.google.co.jp&quot; rel=&quot;nofollow&quot;&gt;himeko24&lt;/a&gt;"},{"from_user_id_str":"119988932","profile_image_url":"http://a3.twimg.com/profile_images/1214344633/image_normal.jpg","created_at":"Fri, 25 Feb 2011 17:37:16 +0000","from_user":"7na_love_6sa","id_str":"41190002000007169","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u30d6\u30ed\u30b0\u66f4\u65b0\u3057\u307e\u3057\u305f\uff01Twitter\uff06mixi\u304b\u3089\u3082\u30b3\u30e1\u30f3\u30c8\u5b9c\u3057\u304f\u306d\u266a\n\u300c\u30cd\u30a4\u30eb\u30c1\u30a7\u30f3\u30b8\u266a\u300d http://amba.to/hy0Do2","id":41190002000007169,"from_user_id":119988932,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"217408835","profile_image_url":"http://a1.twimg.com/profile_images/1215428655/____4.0157_normal.jpg","created_at":"Fri, 25 Feb 2011 17:37:12 +0000","from_user":"nikubenki1123","id_str":"41189985424125952","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @x68k: \u57fa\u672c\u7684\u306bTwitter\u4e0a\u3067\u306f&quot;\u597d\u304d\u306b\u3084\u308c\u3070\u3044\u3044&quot;\u3060\u3051\u3069\u3001\u540d\u524d\u3082\u30a2\u30a4\u30b3\u30f3\u3082\u30a2\u30f3\u30bf\u3058\u3083\u306a\u3044\u305f\u304f\u3055\u3093\u306e\u4eba\u305f\u3061\u306e\u9b42\u304c\u3053\u3082\u3063\u305f\u3082\u306e\u306a\u306e\u3060\u304b\u3089\u3001\u305d\u308c\u3060\u3051\u306f&quot;\u80cc\u8ca0\u3048&quot;\u3068\u601d\u3046\uff1e\u30ad\u30e3\u30e9\u30af\u30bf\u30fc\u30a2\u30ab\u30a6\u30f3\u30c8","id":41189985424125952,"from_user_id":217408835,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://tweetlogix.com&quot; rel=&quot;nofollow&quot;&gt;Tweetlogix&lt;/a&gt;"},{"from_user_id_str":"139137996","profile_image_url":"http://a0.twimg.com/profile_images/1087408006/cut_work_18_normal.jpg","created_at":"Fri, 25 Feb 2011 17:37:12 +0000","from_user":"fujiokayouko","id_str":"41189984354566144","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u591c\u4e2d\u306bTwitter\u3092\u306a\u3093\u3068\u306a\u304f\u8997\u3044\u305f\u3089\u3082\u306e\u3059\u3054\u3044\u307e\u3069\u30de\u30aeTL\u3067","id":41189984354566144,"from_user_id":139137996,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://mobile.twitter.com&quot; rel=&quot;nofollow&quot;&gt;Mobile Web&lt;/a&gt;"},{"from_user_id_str":"61221002","profile_image_url":"http://a3.twimg.com/profile_images/554916301/_1259757374_61_normal.png","created_at":"Fri, 25 Feb 2011 17:37:08 +0000","from_user":"hiromk63","id_str":"41189970613903360","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @kenji_kohashi \u7121\u4e8b\u6620\u753b\u88fd\u4f5c\u306e\u5831\u544a\u306f\u3067\u304d\u305f\u3051\u3069\u4eca\u3060\u7de8\u96c6\u306f\u7d9a\u3044\u3066\u307e\u3059w \u305d\u3093\u306a\u3068\u3053\u3067 \u6620\u753b\u300cDON'T STOP!\u300d\u306eTwitter \u30a2\u30ab\u30a6\u30f3\u30c8 @DONTSTOPMOVIE \u3082\u958b\u59cb\u3001\u3082\u3057\u826f\u304b\u3063\u305f\u3089\u30d5\u30a9\u30ed\u30fc\u3057\u3066\u304f\u3060\u3055\u3044\uff01\u50d5\u3082\u542b\u3081\u6620\u753b\u88fd\u4f5c\u95a2\u4fc2\u8005\u304c\u6c17\u9577\u306b\u3064\u3076\u3084\u304d\u307e\u3059","id":41189970613903360,"from_user_id":61221002,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.hootsuite.com&quot; rel=&quot;nofollow&quot;&gt;HootSuite&lt;/a&gt;"},{"from_user_id_str":"147393812","profile_image_url":"http://a1.twimg.com/profile_images/1114001122/hana_10_normal.jpg","created_at":"Fri, 25 Feb 2011 17:37:07 +0000","from_user":"tubasa_uki","id_str":"41189965924667392","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\uff10\uff10\uff17\u3082\u3073\u3063\u304f\u308a\u3000\u5927\u56fd\u306e\u5927\u7d71\u9818\u9078\u304b\u3089\u3000\u5cf6\u56fd\u306e\u5730\u65b9\u9078\u306e\u9078\u6319\u307e\u3067\u306b\u3082\u3000\u6697\u8e8d\u3057\u3066\u3044\u308b\uff3e\uff3e\u3000\u3053\u306eTwitter \u30b9\u30ad\u30e3\u30f3\u30c0\u30eb\u3084\u60aa\u8cea\u306a\u4e8b\u4ef6\u306b\u3082\u7d61\u3080\u304c\u3000Twitter\u304b\u3089\u767a\u4fe1\u3055\u308c\u305f\u3000\u5e73\u548c\u3078\u306e\u8ca2\u732e\u306f\u3000\u307e\u3055\u306b\u30ce\u30fc\u3079\u30eb\u5e73\u548c\u8cde\u3082\u306e http://bit.ly/esjWDG #dotubo_ss","id":41189965924667392,"from_user_id":147393812,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twittbot.net/&quot; rel=&quot;nofollow&quot;&gt;twittbot.net&lt;/a&gt;"},{"from_user_id_str":"96081746","profile_image_url":"http://a0.twimg.com/profile_images/1244505419/illust832_normal.png","created_at":"Fri, 25 Feb 2011 17:37:07 +0000","from_user":"kazukt","id_str":"41189963131387904","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3046\u308b\u304a\u307c\u7d75\u3001\u4ed6\u306e\u4eba\u306e\u3082\u898b\u3066\u3044\u305f\u3089\u3084\u305f\u3089\u30de\u30f3\u30ac\u306e\u30ad\u30e3\u30e9\u30af\u30bf\u30fc\u3067\u3080\u3061\u3083\u304f\u3061\u3083\u4e0a\u624b\u3044\u4eba\u304c\uff01\u2026\u3068\u3001\u30d7\u30ed\u30d5\u30a3\u30fc\u30eb\u898b\u305f\u3089\u3001\u306a\u3093\u3068\u3054\u672c\u4eba\u304c\u66f8\u3044\u3066\u3044\u3089\u3063\u3057\u3083\u3063\u305f\u3002Twitter\u3063\u3066\u30b9\u30b4\u30a4\u308f\u3002","id":41189963131387904,"from_user_id":96081746,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://janetter.net/&quot; rel=&quot;nofollow&quot;&gt;Janetter&lt;/a&gt;"},{"from_user_id_str":"86306230","profile_image_url":"http://a1.twimg.com/profile_images/593004766/j2j_normal.jpg","created_at":"Fri, 25 Feb 2011 17:37:03 +0000","from_user":"excite_j2j","id_str":"41189949927596032","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u79c1\u306f\u3001\u65e9\u7a32\u7530\u5927\u5b66\u30aa\u30fc\u30d7\u30f3\u30ab\u30ec\u30c3\u30b8\u306e\u5b66\u751f\u306e\u8003\u3048\u3066\u3044\u308b\u3001\u5352\u696d\u751fSoudai /\u65e9\u7a32\u7530\u30ab\u30fc\u30c9\u4f1a\u54e1/ Soudai\u5b66\u751f\u89aa/\u3001\u8ab0\u304b\u304c\u30aa\u30fc\u30d7\u30f3\u30ab\u30ec\u30c3\u30b8\u65e9\u7a32\u7530\u5927\u5b662000\u5186\u306e\u5165\u5834\u6599\u306e\u30e1\u30f3\u30d0\u30fc\u3092\u7d39\u4ecb\u3057\u3066\u304f\u308c\u305f\u65b9\u304c\u305a\u3063\u3068\u5b89\u4e0a\u304c\u308a\u3060\u3002 (\u5143\u767a\u8a00 http://bit.ly/esxiwT )","id":41189949927596032,"from_user_id":86306230,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://d.hatena.ne.jp/fn7&quot; rel=&quot;nofollow&quot;&gt;\u65e5\u672c\u8a9e\u65e5\u672c\u8a9e\u7ffb\u8a33\u30b8\u30a7\u30cd\u30ec\u30fc\u30bf&lt;/a&gt;"},{"from_user_id_str":"120927161","profile_image_url":"http://a0.twimg.com/profile_images/1230006483/20110131_13022_26776_normal.jpg","created_at":"Fri, 25 Feb 2011 17:37:00 +0000","from_user":"FRISKFOSSILFANG","id_str":"41189933855158272","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @syu_thi_bot: \u3044\u3064\u307e\u3067Twitter\u3084\u3063\u3066\u308b\u3093\u3060\u3044\uff1f\u3044\u3044\u52a0\u6e1b\u73fe\u5b9f\u306b\u623b\u308a\u306a\u3088\u3002\u3053\u3053\u306f\u30ad\u30df\u305f\u3061\u4e09\u6b21\u5143\u306e\u4eba\u9593\u304c\u3044\u308b\u3079\u304d\u5834\u6240\u3058\u3083\u306a\u3044\u3093\u3060\u3088\u3002\u305d\u3093\u306a\u306e\u57fa\u672c\u3060\u308d\uff01\uff01","id":41189933855158272,"from_user_id":120927161,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twittbot.net/&quot; rel=&quot;nofollow&quot;&gt;twittbot.net&lt;/a&gt;"},{"from_user_id_str":"228973151","profile_image_url":"http://a1.twimg.com/sticky/default_profile_images/default_profile_0_normal.png","created_at":"Fri, 25 Feb 2011 17:36:59 +0000","from_user":"daisuke1589","id_str":"41189931887890432","metadata":{"result_type":"recent"},"to_user_id":null,"text":"twitter\u3063\u3066\u52dd\u624b\u306b\u30d5\u30a9\u30ed\u30fc\u3057\u3066\u3082\u3044\u3044\u3093\u3060\u3063\u3051\uff1f","id":41189931887890432,"from_user_id":228973151,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"54314110","profile_image_url":"http://a2.twimg.com/profile_images/1193541240/image_normal.jpg","created_at":"Fri, 25 Feb 2011 17:36:57 +0000","from_user":"Alohazuki","id_str":"41189922668953600","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Twitter\u306e\u5e83\u5cf6\u5f01\u3082\u3042\u3063\u305f\u3093\u3060\u306d\u3002","id":41189922668953600,"from_user_id":54314110,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.flight.co.jp/iPhone/TweetMe/&quot; rel=&quot;nofollow&quot;&gt;TweetMe for iPhone&lt;/a&gt;"},{"from_user_id_str":"72594681","profile_image_url":"http://a3.twimg.com/profile_images/1227240916/____normal.jpg","created_at":"Fri, 25 Feb 2011 17:36:56 +0000","from_user":"uri4ichi","id_str":"41189916922744832","metadata":{"result_type":"recent"},"to_user_id":226852303,"text":"@7_fuka Twitter\u304b\u3076\u308c\u306f\u3001\u3059\u3050\u306bD\u306b\u8d70\u308b\u306e\u3067\u3002\u306a\u3093\u3060\u304b\u5fae\u7b11\u307e\u3057\u3044\u306e\u3067\u3059( \u2579\u25e1\u2579)","id":41189916922744832,"from_user_id":72594681,"to_user":"7_fuka","geo":null,"iso_language_code":"ja","to_user_id_str":"226852303","source":"&lt;a href=&quot;http://sites.google.com/site/yorufukurou/&quot; rel=&quot;nofollow&quot;&gt;YoruFukurou&lt;/a&gt;"},{"from_user_id_str":"168393854","profile_image_url":"http://a3.twimg.com/profile_images/1253554518/GuNp3xVS_normal","created_at":"Fri, 25 Feb 2011 17:36:55 +0000","from_user":"makonyanhime","id_str":"41189913701515264","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6589\u85e4\u3055\u3093\u3082\u30c0\u30e1\u3060...\u4eca\u591c\u306fTwitter\u5909\u3060\u306a\u3041(;_;)","id":41189913701515264,"from_user_id":168393854,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://mobile.twitter.com&quot; rel=&quot;nofollow&quot;&gt;Twitter for Android&lt;/a&gt;"},{"from_user_id_str":"122636787","profile_image_url":"http://a2.twimg.com/profile_images/1189607563/DSC01987_normal.JPG","created_at":"Fri, 25 Feb 2011 17:36:54 +0000","from_user":"enpy1217","id_str":"41189908500594688","metadata":{"result_type":"recent"},"to_user_id":null,"text":"twitter\u4e45\u3057\u3076\u308a\u306b\u958b\u3044\u305f\u3002\u3068\u3066\u3082\u591a\u5fd9\u3060\u3063\u305f\u3002\u75b2\u308c\u305f\u3002\u304a\u98a8\u5442\u306b\u3082\u5165\u308c\u306a\u304b\u3063\u305f\u3002\u30ad\u30bf\u30cd\u2026","id":41189908500594688,"from_user_id":122636787,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.flight.co.jp/iPhone/TweetMe/&quot; rel=&quot;nofollow&quot;&gt;TweetMe for iPhone&lt;/a&gt;"},{"from_user_id_str":"81663694","profile_image_url":"http://a3.twimg.com/profile_images/1177576126/__normal.png","created_at":"Fri, 25 Feb 2011 17:36:51 +0000","from_user":"progmiya","id_str":"41189895494049792","metadata":{"result_type":"recent"},"to_user_id":94050844,"text":"@hirasai_skyhigh \u6674\u308c\u541b\u3002\u30cd\u30bf\u30d0\u30ec\u306f\u898b\u305f\u304f\u306a\u3044\u3051\u3069\u30012ch\u3082twitter\u3082\u3084\u308a\u305f\u3044\u306e\u304c\u4eba\u3068\u8a00\u3046\u3082\u306e\u3088","id":41189895494049792,"from_user_id":81663694,"to_user":"hirasai_skyhigh","geo":null,"iso_language_code":"ja","to_user_id_str":"94050844","source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"159237469","profile_image_url":"http://a3.twimg.com/profile_images/1140433484/______2_normal.png","created_at":"Fri, 25 Feb 2011 17:36:50 +0000","from_user":"hikari_juku","id_str":"41189892096532480","metadata":{"result_type":"recent"},"to_user_id":135634241,"text":"@sssukimasuky \n\u4f55\u304b\u3042\u3063\u305f\u98a8\u3067\u3059\u306d\uff08\u7b11\uff09\u3000\u78ba\u304b\u306bTwitter\u306f\u6c17\u8efd\u3067\u3059\u3057\u306d\u3002\u30e1\u30fc\u30eb\u306b\u306a\u308b\u3068\u30d5\u30c3\u30c8\u30ef\u30fc\u30af\u304c\u91cd\u304f\u306a\u308b\u5834\u5408\u3082\u3042\u308b\u304b\u3082\u3057\u308c\u307e\u305b\u3093\u306d\u3002\u305d\u3046\u8003\u3048\u308b\u3068\u3059\u3054\u3044\u6642\u4ee3\u3060\u3002","id":41189892096532480,"from_user_id":159237469,"to_user":"sssukimasuky","geo":null,"iso_language_code":"ja","to_user_id_str":"135634241","source":"&lt;a href=&quot;http://www.hootsuite.com&quot; rel=&quot;nofollow&quot;&gt;HootSuite&lt;/a&gt;"},{"from_user_id_str":"141915990","profile_image_url":"http://a0.twimg.com/profile_images/1248969898/icon679566266564378764images_normal.jpg","created_at":"Fri, 25 Feb 2011 17:36:48 +0000","from_user":"adashino_","id_str":"41189886878949376","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @fuefukioyasumi: \u3082\u3046\u3044\u3044\u3002\u300c\u671d\u751f\u300d\u306a\u3093\u304b\u3044\u3044\u3002BBC\u3082CNN\u3082\u82f1\u8a9e\u653e\u9001\u3060\u3002\u307f\u3093\u306a\u3001\u30c6\u30ec\u30d3\u3092\u3076\u3061\u3063\u3068\u5207\u3063\u3066Twitter\u3092\u898b\u3088\u3046\u3002\u3053\u3053\u306b\u300c\u751f\u304d\u305f\u60c5\u5831\u300d\u304c\u6d41\u308c\u3066\u3044\u308b\u3002\u300c\u751f\u304d\u305f\u53eb\u3073\u300d\u304c\u3042\u308b\u3002\u3053\u3053\u304b\u3089\u3067\u3082\u6b74\u53f2\u306e\u8ee2\u63db\u70b9\u3092\u611f\u3058\u308b\u4e8b\u304c\u3067\u304d\u308b\u306e\u3060\u3002 @gjmorley #libjp","id":41189886878949376,"from_user_id":141915990,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"111308629","profile_image_url":"http://a1.twimg.com/profile_images/1137421845/mmooton_normal.png","created_at":"Fri, 25 Feb 2011 17:36:46 +0000","from_user":"mmooton","id_str":"41189877940748288","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @minponjp: \u3010\u3064\u3076\u3084\u304f\u3060\u3051\u306710000\u5186\u5546\u54c1\u5238GET\u3011&lt;&lt;visa\u30ae\u30d5\u30c8\u30ab\u30fc\u30c910000\u5186\u5206\u3092\u6bce\u6708\u62bd\u9078\u3067\u30d7\u30ec\u30bc\u30f3\u30c8&gt;&gt; \u21d2\u8a73\u3057\u304f\u306f\u4e0b\u306e\uff35\uff32\uff2c\u3088\u308a\u2193\u2193\u2193\u2193\u2193 http://minpon.jp/user_data/twitter_campaign.php","id":41189877940748288,"from_user_id":111308629,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twittbot.net/&quot; rel=&quot;nofollow&quot;&gt;twittbot.net&lt;/a&gt;"},{"from_user_id_str":"140945098","profile_image_url":"http://a0.twimg.com/profile_images/1198948281/b7664c71-87bc-4187-9d46-a61ab8d41b96_normal.jpg","created_at":"Fri, 25 Feb 2011 17:36:46 +0000","from_user":"waruimayuko","id_str":"41189877122867200","metadata":{"result_type":"recent"},"to_user_id":113371008,"text":"@toumeisyoujyo \u3042\u3053\u3061\u3083\u3093Twitter\u3084\u3063\u3066\u305f\u306e\u306d\u3002\u307e\u3060\u304a\u5e2d\u3042\u308b\u305d\u3046\u306a\u306e\u3067\u662f\u975e\uff01","id":41189877122867200,"from_user_id":140945098,"to_user":"toumeisyoujyo","geo":null,"iso_language_code":"ja","to_user_id_str":"113371008","source":"&lt;a href=&quot;http://twimi.jp/?r=via&quot; rel=&quot;nofollow&quot;&gt;twimi\u2606new&lt;/a&gt;"},{"from_user_id_str":"167512527","profile_image_url":"http://a1.twimg.com/profile_images/1172286365/newspaper_normal.jpg","created_at":"Fri, 25 Feb 2011 17:36:45 +0000","from_user":"KoranKaget","id_str":"41189871775252480","metadata":{"result_type":"recent"},"to_user_id":null,"text":"http://bit.ly/93Eeud RT @motocentrism \u30d6\u30ed\u30b0\u8a18\u4e8b\u66f8\u304d\u307e\u3057\u305f\u30fc\uff1a MotoGP\u3000\u30d6\u30c3\u30af\u30e1\u30fc\u30ab\u30fc\u306b\u898b\u308b\u5404\u30e9\u30a4\u30c0\u30fc\u306e\u4e0b\u99ac\u8a55 - http://goo.gl/xVe6... http://bit.ly/hDYxyx @motogpudpate","id":41189871775252480,"from_user_id":167512527,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"160251772","profile_image_url":"http://a3.twimg.com/profile_images/1150692471/_____normal.jpg","created_at":"Fri, 25 Feb 2011 17:36:45 +0000","from_user":"keikochaki","id_str":"41189870781075456","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u300c\u305a\u3063\u3068twitter\u3084\u3063\u3066\u308b\u3088\u306d\u300d\u3068\u6012\u3089\u308c\u3066\u3057\u307e\u3063\u305f(\u82e6\u7b11)","id":41189870781075456,"from_user_id":160251772,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twtr.jp&quot; rel=&quot;nofollow&quot;&gt;Keitai Web&lt;/a&gt;"},{"from_user_id_str":"211565766","profile_image_url":"http://a2.twimg.com/sticky/default_profile_images/default_profile_5_normal.png","created_at":"Fri, 25 Feb 2011 17:36:42 +0000","from_user":"sayap1yo","id_str":"41189858236043264","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u304a\u98df\u4e8b\u4f1a\u884c\u304d\u305f\u304b\u3063\u305f\u3051\u3069\n\u53cb\u9054\u3068\u904a\u3093\u3067\u307e\u3093\u305f\ue056\ue326\n\nTwitter\u898b\u3066\u307e\u3057\u305f\u3051\u3069\n\u3064\u3076\u3084\u304f\u30bf\u30a4\u30df\u30f3\u30b0\n\u5931\u3063\u3066\u305f\u3060\u3051\u3067\u3059( \u00b4 \u25bd ` )\uff89\u7b11","id":41189858236043264,"from_user_id":211565766,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.nibirutech.com&quot; rel=&quot;nofollow&quot;&gt;TwitBird&lt;/a&gt;"},{"from_user_id_str":"19603191","profile_image_url":"http://a1.twimg.com/profile_images/1254077264/116_normal.gif","created_at":"Fri, 25 Feb 2011 17:36:40 +0000","from_user":"Purple_Flash","id_str":"41189851873157120","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @syu_thi_bot: \u3044\u3064\u307e\u3067Twitter\u3084\u3063\u3066\u308b\u3093\u3060\u3044\uff1f\u3044\u3044\u52a0\u6e1b\u73fe\u5b9f\u306b\u623b\u308a\u306a\u3088\u3002\u3053\u3053\u306f\u30ad\u30df\u305f\u3061\u4e09\u6b21\u5143\u306e\u4eba\u9593\u304c\u3044\u308b\u3079\u304d\u5834\u6240\u3058\u3083\u306a\u3044\u3093\u3060\u3088\u3002\u305d\u3093\u306a\u306e\u57fa\u672c\u3060\u308d\uff01\uff01","id":41189851873157120,"from_user_id":19603191,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twittbot.net/&quot; rel=&quot;nofollow&quot;&gt;twittbot.net&lt;/a&gt;"},{"from_user_id_str":"215930291","profile_image_url":"http://a0.twimg.com/profile_images/1247906106/image_normal.jpg","created_at":"Fri, 25 Feb 2011 17:36:30 +0000","from_user":"minori_millie","id_str":"41189810055938048","metadata":{"result_type":"recent"},"to_user_id":212972774,"text":"@c_c_chika \u7b11\u3063\u3066\u305d\u3046\u3060\u306a\u3063\u3066\u601d\u3063\u3066\u305f(o^^o)\u30c1\u30ab\u306e\u7b11\u3044\u58f0\u304c\u805e\u3053\u3048\u3066\u304d\u305f\u3082\u3093\u3002mail\u306bTwitter\u3067\u5927\u5fd9\u3057\u3084\u3002","id":41189810055938048,"from_user_id":215930291,"to_user":"c_c_chika","geo":null,"iso_language_code":"ja","to_user_id_str":"212972774","source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"197459334","profile_image_url":"http://a3.twimg.com/profile_images/1210698414/shiratorishoko_normal.png","created_at":"Fri, 25 Feb 2011 17:36:30 +0000","from_user":"SyokoShiratori","id_str":"41189809586311168","metadata":{"result_type":"recent"},"to_user_id":null,"text":"+0.90kg \u30c0\u30a4\u30a8\u30c3\u30c8\u5fdc\u63f4\u3057\u3066\u307e\u3059\u3002(*^^*)\u30c0\u30a4\u30a8\u30c3\u30c8\u4f53\u91cd\u5831\u544a\u306e\u3064\u3076\u3084\u304d\u3067\u3059\u3002 http://bit.ly/fhN2fM RT @hayayumi \u3053\u3093\u306a\u6642\u9593\u306b\u3084\u304d\u306b\u304f\u30fc\u266b\u30c0\u30a4\u30a8\u30c3\u30c8\u671f\u9593\u306a\u306e\u306b\u306d....","id":41189809586311168,"from_user_id":197459334,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://mob-mc.com&quot; rel=&quot;nofollow&quot;&gt;\uff08\u4eee\u79f0\uff09\u30c4\u30a4\u30af\u30ea\u30c3\u30af twiclick&lt;/a&gt;"},{"from_user_id_str":"12337665","profile_image_url":"http://a0.twimg.com/profile_images/1146194926/pixiv-icon_normal.png","created_at":"Fri, 25 Feb 2011 17:36:27 +0000","from_user":"t_a_k_i","id_str":"41189797364121600","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Google\u30ea\u30fc\u30c0\u30fc\u3067\u30b5\u30a4\u30c8\u306e\u66f4\u65b0\u30c1\u30a7\u30c3\u30af\u3059\u308b\u3088\u308a\u3001Twitter\u30a2\u30ab\u30a6\u30f3\u30c8\u3092\u30d5\u30a9\u30ed\u30fc\u3057\u3066\u66f4\u65b0\u3092\u77e5\u308b\u307b\u3046\u304c\u4fbf\u5229\u3060\u306a\u3053\u308c","id":41189797364121600,"from_user_id":12337665,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://sites.google.com/site/yorufukurou/&quot; rel=&quot;nofollow&quot;&gt;YoruFukurou&lt;/a&gt;"},{"from_user_id_str":"104862453","profile_image_url":"http://a3.twimg.com/profile_images/1248609864/Image014_normal.jpg","created_at":"Fri, 25 Feb 2011 17:36:25 +0000","from_user":"yuz_ume","id_str":"41189787943714816","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @fuefukioyasumi: \u3082\u3046\u3044\u3044\u3002\u300c\u671d\u751f\u300d\u306a\u3093\u304b\u3044\u3044\u3002BBC\u3082CNN\u3082\u82f1\u8a9e\u653e\u9001\u3060\u3002\u307f\u3093\u306a\u3001\u30c6\u30ec\u30d3\u3092\u3076\u3061\u3063\u3068\u5207\u3063\u3066Twitter\u3092\u898b\u3088\u3046\u3002\u3053\u3053\u306b\u300c\u751f\u304d\u305f\u60c5\u5831\u300d\u304c\u6d41\u308c\u3066\u3044\u308b\u3002\u300c\u751f\u304d\u305f\u53eb\u3073\u300d\u304c\u3042\u308b\u3002\u3053\u3053\u304b\u3089\u3067\u3082\u6b74\u53f2\u306e\u8ee2\u63db\u70b9\u3092\u611f\u3058\u308b\u4e8b\u304c\u3067\u304d\u308b\u306e\u3060\u3002 @gjmorley #libjp","id":41189787943714816,"from_user_id":104862453,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"213846746","profile_image_url":"http://a1.twimg.com/profile_images/1242263342/haruhi1_normal.png","created_at":"Fri, 25 Feb 2011 17:36:23 +0000","from_user":"sloth888jpgame","id_str":"41189778409922560","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u643a\u5e2f\u96fb\u8a71\u7528\u306eTwitter\u3092\u5229\u7528\u3057\u305f\u30b2\u30fc\u30e0\u3092\u77e5\u3063\u3066\u3044\u308b\u4eba\u306f\u3001\u7d39\u4ecb\u3057\u3066\u304f\u3060\u3055\u3044\u3002 #TwitterGame","id":41189778409922560,"from_user_id":213846746,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twittbot.net/&quot; rel=&quot;nofollow&quot;&gt;twittbot.net&lt;/a&gt;"},{"from_user_id_str":"17484724","profile_image_url":"http://a2.twimg.com/profile_images/1138677235/iconue_normal.jpg","created_at":"Fri, 25 Feb 2011 17:36:22 +0000","from_user":"tsutcho","id_str":"41189777831231490","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u30a2\u30aa\u30b7\u30de\u3055\u3093twitter\u306b\u7acb\u3064\u30fb\u30fb\u30fb\u3060\u3068\u30fb\u30fb\u30fb\uff1f\uff12\u756a\u76ee\u306e\u30d5\u30a9\u30ed\u30ef\u30fc\u306e\u5ea7\u3092\u3044\u305f\u3060\u3044\u3066\u304a\u3053\u3046","id":41189777831231490,"from_user_id":17484724,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"220041064","profile_image_url":"http://a1.twimg.com/sticky/default_profile_images/default_profile_0_normal.png","created_at":"Fri, 25 Feb 2011 17:36:21 +0000","from_user":"sloth888jp_bot5","id_str":"41189772521254914","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6642\u3005\u3001TwiAll\uff08\u30c4\u30a4\u30aa\u30fc\u30eb\uff09\u3067Twitter\u306e\u81ea\u52d5\u30d5\u30a9\u30ed\u30fc\u30fb\u30d5\u30a9\u30ed\u30fc\u8fd4\u3057\u3092\u3057\u3066\u3044\u307e\u3059\u3002","id":41189772521254914,"from_user_id":220041064,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twittbot.net/&quot; rel=&quot;nofollow&quot;&gt;twittbot.net&lt;/a&gt;"},{"from_user_id_str":"177693537","profile_image_url":"http://a0.twimg.com/profile_images/1240130924/20110207213206_normal.jpg","created_at":"Fri, 25 Feb 2011 17:36:21 +0000","from_user":"Na_Okinawa","id_str":"41189769912397824","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @ebizo66: \u30ea\u30d3\u30a2\u30fb\u30c8\u30ea\u30dd\u30ea\u306eTw\uff1a\u6551\u6025\u8eca\u304c\u6765\u305f\u304c\u3001\u8ca0\u50b7\u8005\u3092\u8eca\u5185\u3067\u6bba\u5bb3\u3057\u3066\u3044\u305f\u3001\u3068\u3002\uff08\u6ec5\u8336\u82e6\u8336\u3060\uff01\uff09http://ow.ly/43pQ2 #libjp","id":41189769912397824,"from_user_id":177693537,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.hootsuite.com&quot; rel=&quot;nofollow&quot;&gt;HootSuite&lt;/a&gt;"},{"from_user_id_str":"147006592","profile_image_url":"http://a2.twimg.com/profile_images/1251306448/image_normal.jpg","created_at":"Fri, 25 Feb 2011 17:36:19 +0000","from_user":"takasu_rika","id_str":"41189763532865536","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @kihirokiro: Twitter\u3067\u30cd\u30bf\u30d0\u30ec\u4e91\u3005\u3044\u3063\u3066\u3082\u5143\u3005Twitter\u3063\u3066\u300c\u500b\u4eba\u306e\u72ec\u308a\u8a00\u300d\u3060\u308d","id":41189763532865536,"from_user_id":147006592,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"100239985","profile_image_url":"http://a1.twimg.com/profile_images/733461294/CIMG0207_normal.JPG","created_at":"Fri, 25 Feb 2011 17:36:19 +0000","from_user":"strangebarjun","id_str":"41189762182152192","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Twitter\u3092\u59cb\u3081\u3066\u3001\u305d\u308d\u305d\u308d\u4e00\u5e74\u306b\u306a\u308a\u307e\u3059\u3002\u632f\u308a\u8fd4\u308b\u3068\u3001\u307c\u304f\u306e\u4eba\u9593\u6027\u3092\u305d\u3063\u304f\u308a\u53cd\u6620\u3059\u308b\u304c\u5982\u304f\u3001\u534a\u7aef\u3067\u3059\u306d\u3002\u60c5\u5831\u53ce\u96c6\u30c4\u30fc\u30eb\u3001\u60c5\u5831\u767a\u4fe1\u30c4\u30fc\u30eb\u3001\u30b3\u30df\u30e5\u30cb\u30b1\u30fc\u30b7\u30e7\u30f3\u30c4\u30fc\u30eb\u3001\u3069\u308c\u3082\u6a5f\u80fd\u3057\u5207\u308c\u3066\u3044\u306a\u3044\u306a\u3042\u3001\u3068\u3002\u305d\u3057\u3066\u3042\u308b\u306e\u306f\u300c\u3069\u3053\u304b\u3089\u3082\u76f8\u624b\u306b\u3055\u308c\u3066\u3044\u306a\u3044\u300d\u3068\u3044\u3046\u3001\u65e2\u77e5\u306e\u73fe\u5b9f\u3067\u3059\u3002","id":41189762182152192,"from_user_id":100239985,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.echofon.com/&quot; rel=&quot;nofollow&quot;&gt;Echofon&lt;/a&gt;"},{"from_user_id_str":"126390384","profile_image_url":"http://a2.twimg.com/profile_images/1176065038/nemunemu_normal.jpg","created_at":"Fri, 25 Feb 2011 17:36:18 +0000","from_user":"gesukapper","id_str":"41189757476286464","metadata":{"result_type":"recent"},"to_user_id":80935865,"text":"@tatsugorou \u306a\u3093\u304b\u4e16\u306e\u4e2d\u306b\u306f\u611a\u75f4\u3082\u4e0b\u30cd\u30bf\u3082\u8a00\u3044\u653e\u984c\u306etwitter\u3068\u3044\u3046\u3082\u306e\u304c\u3042\u308b\u305d\u3046\u3067\u3059\u3002","id":41189757476286464,"from_user_id":126390384,"to_user":"tatsugorou","geo":null,"iso_language_code":"ja","to_user_id_str":"80935865","source":"&lt;a href=&quot;http://janetter.net/&quot; rel=&quot;nofollow&quot;&gt;Janetter&lt;/a&gt;"},{"from_user_id_str":"196059029","profile_image_url":"http://a1.twimg.com/profile_images/1208831726/ruka2_normal.jpg","created_at":"Fri, 25 Feb 2011 17:36:17 +0000","from_user":"luka_m_bot","id_str":"41189756666777600","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u308f\u305f\u3057\u3082twitter\u306f\u3058\u3081\u307e\u3057\u305f","id":41189756666777600,"from_user_id":196059029,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www24.atpages.jp/aoi2/bot.php&quot; rel=&quot;nofollow&quot;&gt;\u5de1\u308b\u97f3&lt;/a&gt;"},{"from_user_id_str":"88135613","profile_image_url":"http://a3.twimg.com/profile_images/1088552877/P9280150_normal.JPG","created_at":"Fri, 25 Feb 2011 17:36:14 +0000","from_user":"Roko38","id_str":"41189740321587200","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\uff46\uff42\u306b\u30ed\u30b0\u30a4\u30f3\u3059\u308b\u6a5f\u4f1a\u304c\u65e5\u306b\u65e5\u306b\u5897\u3048\u3066\u304d\u305f\uff3e\uff3e \u6163\u308c\u3066\u304f\u308b\u3068\u3042\u3063\u3061\u306e\u304c\u9762\u767d\u3044\uff06\u4f7f\u3044\u52dd\u624b\u304c\u3044\u3044\u3088\u3046\u306a\u6c17\u304c\u3059\u308b\u3002\u8907\u5408\u7684\u306b\u8272\u3005\u51fa\u6765\u307e\u3059\u3088\u306d\u2026\u3063\u3066\u3001\u601d\u3063\u305f\u3053\u3068\u3092\u545f\u304f\u306e\u306fTwitter\u3060\u3063\u305f\u308a\u3067\u3059\u304c\u2026(^_^; \u305d\u3057\u3066\u81ea\u5206\u306e\u5834\u5408\u306f\u30c4\u30a4\u3068\u9023\u52d5\u3057\u3065\u3089\u3044\u72b6\u6cc1\u3060\u3063\u305f\u308a\u3067\u2026\u3002","id":41189740321587200,"from_user_id":88135613,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"110825211","profile_image_url":"http://a2.twimg.com/profile_images/1108977866/o_t_normal.jpg","created_at":"Fri, 25 Feb 2011 17:36:12 +0000","from_user":"kou_nanjyo","id_str":"41189731941363712","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @x68k: \u57fa\u672c\u7684\u306bTwitter\u4e0a\u3067\u306f&quot;\u597d\u304d\u306b\u3084\u308c\u3070\u3044\u3044&quot;\u3060\u3051\u3069\u3001\u540d\u524d\u3082\u30a2\u30a4\u30b3\u30f3\u3082\u30a2\u30f3\u30bf\u3058\u3083\u306a\u3044\u305f\u304f\u3055\u3093\u306e\u4eba\u305f\u3061\u306e\u9b42\u304c\u3053\u3082\u3063\u305f\u3082\u306e\u306a\u306e\u3060\u304b\u3089\u3001\u305d\u308c\u3060\u3051\u306f&quot;\u80cc\u8ca0\u3048&quot;\u3068\u601d\u3046\uff1e\u30ad\u30e3\u30e9\u30af\u30bf\u30fc\u30a2\u30ab\u30a6\u30f3\u30c8","id":41189731941363712,"from_user_id":110825211,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://tweetlogix.com&quot; rel=&quot;nofollow&quot;&gt;Tweetlogix&lt;/a&gt;"},{"from_user_id_str":"2095960","profile_image_url":"http://a2.twimg.com/profile_images/1248999075/majiresu2_normal.jpg","created_at":"Fri, 25 Feb 2011 17:36:10 +0000","from_user":"guldeen","id_str":"41189726945947648","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3042\u308a\u3083\u307e\u3041\u3002\u25bchttp://bit.ly/et4vVa \uff3b\u89d2\u5ddd\u66f8\u5e97\uff3d\u300c\u30b6\u30fb\u30b9\u30cb\u30fc\u30ab\u30fc\u300d\u4f11\u520a\u3078\u3000\u300c\u6dbc\u5bae\u30cf\u30eb\u30d2\u300d\u751f\u3093\u3060\u30e9\u30ce\u30d9\u96d1\u8a8c18\u5e74\u3067\u5e55 via http://twitter.com/lkj777","id":41189726945947648,"from_user_id":2095960,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"164121549","profile_image_url":"http://a1.twimg.com/profile_images/1251758789/m_108-07ae3_normal.jpg","created_at":"Fri, 25 Feb 2011 17:36:09 +0000","from_user":"hachi_touhi","id_str":"41189719299727360","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u30cd\u30bf\u30d0\u30ec\u6c17\u306b\u3057\u3066\u305f\u3089Twitter\u3084\u3063\u3066\u3089\u308c\u306a\u3044ww","id":41189719299727360,"from_user_id":164121549,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twicca.r246.jp/&quot; rel=&quot;nofollow&quot;&gt;twicca&lt;/a&gt;"},{"from_user_id_str":"194494659","profile_image_url":"http://a3.twimg.com/profile_images/1249717822/image_normal.jpg","created_at":"Fri, 25 Feb 2011 17:36:08 +0000","from_user":"tomonattomo","id_str":"41189715415801856","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3046\u308f\u3001\u660e\u65e5\u671d\u304b\u3089\u30d0\u30a4\u30c8\u3084\u306e\u306b\u306a\u3093\u3060\u3053\u306e\u306d\u3075\u304b\u3057\u3002\u3042\u3001\u591c\u66f4\u304b\u3057\u3002\u30d1\u30d4\u30eb\u30b9\u3068BUMP\u3001\u3048\u307f\u3068Twitter\u306eDM\u3057\u3059\u304e\u305f\u306a\u2026","id":41189715415801856,"from_user_id":194494659,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://mixi.jp/promotion.pl?id=voice_twitter&quot; rel=&quot;nofollow&quot;&gt; mixi \u30dc\u30a4\u30b9 &lt;/a&gt;"},{"from_user_id_str":"97224364","profile_image_url":"http://a3.twimg.com/profile_images/1150074355/fb8bcca60d340862c053a756377bfae8_normal.jpeg","created_at":"Fri, 25 Feb 2011 17:36:06 +0000","from_user":"xxxheat","id_str":"41189709174677504","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Twitter\u5b8c\u5168\u306b\u30d0\u30b0\u3063\u3066\u308borz","id":41189709174677504,"from_user_id":97224364,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"}],"max_id":41190705623732224,"since_id":38643906774044672,"refresh_url":"?since_id=41190705623732224&q=twitter","next_page":"?page=2&max_id=41190705623732224&rpp=100&lang=ja&q=twitter","results_per_page":100,"page":1,"completed_in":0.093336,"warning":"adjusted since_id to 38643906774044672 (), requested since_id was older than allowed -- since_id removed for pagination.","since_id_str":"38643906774044672","max_id_str":"41190705623732224","query":"twitter"}
+ test/json-data/jp50.json view
@@ -0,0 +1,1 @@+{"results":[{"from_user_id_str":"2458313","profile_image_url":"http://a2.twimg.com/profile_images/1203653060/fure091226_normal.png","created_at":"Thu, 27 Jan 2011 20:30:04 +0000","from_user":"19princess","id_str":"30724239224995840","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6d77\u6674\u300c\u672c\u65e5\u306e\u6771\u4eac\u306e\u304a\u5929\u6c17\u306f\u6674\u6642\u3005\u66c7\u3067\u3057\u3087\u3046\u3002\u6700\u9ad8\u6c17\u6e29\u306f8\u5ea6\u3001\u6700\u4f4e\u6c17\u6e29\u306f1\u5ea6\u3067\u3059\u3002\u3042\u306a\u305f\u306e\u4eca\u65e5\u306e\u4eba\u751f\u306b\u3068\u3073\u3063\u304d\u308a\u306e\u304a\u5929\u6c17\u3092\u2665\u300d","id":30724239224995840,"from_user_id":2458313,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://babyprincess.sakura.ne.jp/about/&quot; rel=&quot;nofollow&quot;&gt;\u5929\u4f7f\u5bb6\u306e\u88cf\u5c71&lt;/a&gt;"},{"from_user_id_str":"66578965","profile_image_url":"http://a0.twimg.com/profile_images/1183763267/fossetta_normal.png","created_at":"Thu, 27 Jan 2011 20:00:04 +0000","from_user":"Fossetta_Tokyo","id_str":"30716689779785729","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u304a\u306f\u3088\u3046\u3054\u3056\u3044\u307e\u3059!!\u6771\u4eac\u90fd\u3001\u672c\u65e5\u306e\u304a\u5929\u6c17\u306f\u6674\u6642\u3005\u66c7\u3002\u6700\u9ad8\u6c17\u6e298\u5ea6\u3001\u6700\u4f4e\u6c17\u6e291\u5ea6\u3002\u6771\u4eac\u5730\u65b9\u3067\u306f\u3001\u7a7a\u6c17\u306e\u4e7e\u71e5\u3057\u305f\u72b6\u614b\u304c\u7d9a\u3044\u3066\u3044\u307e\u3059\u3002\u706b\u306e\u53d6\u308a\u6271\u3044\u306b\u6ce8\u610f\u3057\u3066\u4e0b\u3055\u3044\u3002\u4f0a\u8c46\u8af8\u5cf6\u3068\u5c0f\u7b20\u539f\u8af8\u5cf6\u306b\u306f\u3001\u5f37\u98a8\u3001\u6ce2\u6d6a\u3001\u4e7e\u71e5\u3001\u971c\u306e\u6ce8\u610f\u5831\u3092\u767a\u8868\u4e2d\u3067\u3059\u3002","id":30716689779785729,"from_user_id":66578965,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://bit.ly/Fossetta&quot; rel=&quot;nofollow&quot;&gt;\u30d5\u30a9\u30bb\u30c3\u30bf ver.3.1.1&lt;/a&gt;"},{"from_user_id_str":"104041146","profile_image_url":"http://a2.twimg.com/profile_images/863965118/001jwatokyo_normal.jpg","created_at":"Thu, 27 Jan 2011 19:44:33 +0000","from_user":"jwa_tokyo","id_str":"30712787537756160","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6c17\u8c61\u5e81\u304b\u3089\u6771\u4eac\u306e\u5929\u6c17\u4e88\u5831\u304c\u767a\u8868\u3055\u308c\u307e\u3057\u305f\u3000\u305d\u306e\u5929\u6c17\u4e88\u5831\u3092\u3053\u3053\u3067\u3064\u3076\u3084\u3053\u3046\u304b\u306a\u3041\uff5e\uff1f\u3000\u8003\u3048\u4e2d\u3000\u307e\u3066\u6b21\u53f7\uff08\u7b11","id":30712787537756160,"from_user_id":104041146,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.docodemo.jp/twil/&quot; rel=&quot;nofollow&quot;&gt;Twil2 (Tweet Anytime, Anywhere by Mail)&lt;/a&gt;"},{"from_user_id_str":"144500192","profile_image_url":"http://a3.twimg.com/a/1294874399/images/default_profile_3_normal.png","created_at":"Thu, 27 Jan 2011 18:59:33 +0000","from_user":"kyo4to4","id_str":"30701462774358016","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6771\u4eac\u90fd \u516b\u4e08\u5cf6 - \u4eca\u65e5\u306e\u5929\u6c17\u306f\u30fb\u30fb\u30fb\u66c7\u306e\u3061\u6674\u3067\u3059\u306e\uff01","id":30701462774358016,"from_user_id":144500192,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/kyo4to4&quot; rel=&quot;nofollow&quot;&gt;lost-sheep-bot&lt;/a&gt;"},{"from_user_id_str":"165724582","profile_image_url":"http://a0.twimg.com/profile_images/1222842347/163087_1256281064506_1753997852_484926_2761052_n_normal.jpg","created_at":"Thu, 27 Jan 2011 17:39:06 +0000","from_user":"Rifqi_19931020","id_str":"30681213748387840","metadata":{"result_type":"recent"},"to_user_id":113796067,"text":"@CHLionRagbaby \u30b1\u30f3\u3061\u3083\u3093\u3001\u671d\u4eca\u307e\u3067\u306e\u81ea\u5206\u306e\u30db\u30fc\u30e0\u30a8\u30ea\u30a2\u304b\u3089\u307e\u3060\u975e\u5e38\u306b\u5bd2\u3044\u3068\u96e8\u304c\u964d\u3063\u3066\u3044\u305f..\u3069\u306e\u3088\u3046\u306b\u73fe\u5728\u306e\u6771\u4eac\u306e\u5929\u6c17\uff1f","id":30681213748387840,"from_user_id":165724582,"to_user":"CHLionRagbaby","geo":null,"iso_language_code":"ja","to_user_id_str":"113796067","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"120911929","profile_image_url":"http://a0.twimg.com/profile_images/772435076/20100312___capture2_normal.png","created_at":"Thu, 27 Jan 2011 17:28:30 +0000","from_user":"Cirno_fan","id_str":"30678546020040705","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u4eca\u65e5\u306e\u6771\u4eac\u306e\u5929\u6c17\u306f\u3001\u66c7\u6642\u3005\u6674\u3067\u6700\u9ad8\u6c17\u6e29\u306f8\u2103\uff01 \u6700\u4f4e\u6c17\u6e29\u306f2\u2103\u3060\u3063\u305f\u3088\uff01 RT @mimi22999 \uff08\uff65\u2200\uff65\uff09\uff4c\u3001\u865a\u5f31\u306a\u8005\u306b\u3068\u3063\u3066\u3001\u6717\u3089\u304b\u306a\u9854\u306f\u4e0a\u5929\u6c17\u3068\u540c\u3058\u304f\u3089\u3044\u3046\u308c\u3057\u3044\u3082\u306e\u3060\u3002\u30d5\u30e9\u30f3\u30af\u30ea\u30f3 #tenki #bot","id":30678546020040705,"from_user_id":120911929,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://blog.livedoor.jp/fairycirno/archives/34686.html&quot; rel=&quot;nofollow&quot;&gt;\u5e7b\u60f3\u90f7 \u9727\u306e\u6e56&lt;/a&gt;"},{"from_user_id_str":"65976527","profile_image_url":"http://a0.twimg.com/profile_images/452810990/little_italies_____normal.jpg","created_at":"Thu, 27 Jan 2011 17:00:03 +0000","from_user":"heta_weather01","id_str":"30671388754845696","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3010\u30d8\u30bf\u5929\u3011Tere hommikust.\u30a8\u30b9\u30c8\u30cb\u30a2\u3067\u3059\u3002\u4eca\u65e5\u306e\u6771\u4eac\u306e\u5929\u6c17\u306f\u6674\u6642\u3005\u66c7\u3067\u660e\u65e5\u306f\u66c7\u308a\u3067\u3059\u3002\u3061\u306a\u307f\u306b\u50d5\u306e\u3068\u3053\u308d\u3067\u306f\u66c7\u308a\u3067\u6c17\u6e29-7\u2103\u3067\u3059\u3002\u3061\u306a\u307f\u306b\u30b9\u30ab\u30a4\u30d7\u306e\u958b\u767a\u672c\u90e8\u306f\u50d5\u306e\u5bb6\u306b\u3042\u308b\u3093\u3067\u3059\u3002","id":30671388754845696,"from_user_id":65976527,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www15.atpages.jp/~kageyanma/&quot; rel=&quot;nofollow&quot;&gt;\u5730\u7403\u306e\u4e2d&lt;/a&gt;"},{"from_user_id_str":"7278433","profile_image_url":"http://a3.twimg.com/profile_images/1218965303/michael-1_normal.png","created_at":"Thu, 27 Jan 2011 16:50:16 +0000","from_user":"shimohiko","id_str":"30668926035689472","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3042\u308a\u3002\n\u3010\u7f8e\u4eba\u5929\u6c17/\u6771\u4eac\u3011\u7f8e\u4eba\u5929\u6c17\u30ad\u30e3\u30b9\u30bf\u30fc\u306e&quot;\u3055\u3042\u3084\u3093\u3055\u3093&quot;\u306b\u3088\u308b\u3068\u300c1/29(\u571f)\u306f\u304f\u3082\u308a\u3067\u3001\u964d\u6c34\u78ba\u738740%\u3001\u6700\u9ad8\u6c17\u6e29\u306f6\u2103\u3067\u6700\u4f4e\u6c17\u6e29\u306f1\u2103\u3067\u3059\u300d\u7f8e\u4eba\u5929\u6c17\u21d2http://bit.ly/djB8th http://twitpic.com/3twnl0 #bt_tenki","id":30668926035689472,"from_user_id":7278433,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://bijintenki.jp&quot; rel=&quot;nofollow&quot;&gt;bijintenki.jp&lt;/a&gt;"},{"from_user_id_str":"61770","profile_image_url":"http://a3.twimg.com/profile_images/1206955079/tw172a_normal.png","created_at":"Thu, 27 Jan 2011 16:40:52 +0000","from_user":"rsky","id_str":"30666561853333504","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3075\u3068\u591c\u7a7a\u3092\u898b\u4e0a\u3052\u3066\u30aa\u30ea\u30aa\u30f3\u304c\u898b\u3048\u306a\u304f\u3066\u300c\u6771\u4eac\u306b\u306f\u7a7a\u304c\u306a\u3044\u300d\u3068\u667a\u6075\u5b50\u306e\u3088\u3046\u306a\u3053\u3068\u3092\u601d\u3063\u305f\u308f\u3051\u3060\u304c\u3001\u305f\u3076\u3093\u534a\u5206\u3050\u3089\u3044\u306f\u5929\u6c17\u306e\u305b\u3044\u3067\u3059","id":30666561853333504,"from_user_id":61770,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.hootsuite.com&quot; rel=&quot;nofollow&quot;&gt;HootSuite&lt;/a&gt;"},{"from_user_id_str":"103259265","profile_image_url":"http://a0.twimg.com/profile_images/767857973/anime_icon_normal.gif","created_at":"Thu, 27 Jan 2011 16:37:42 +0000","from_user":"liveshowonly","id_str":"30665765698928640","metadata":{"result_type":"recent"},"to_user_id":null,"text":"iPhone\u5929\u6c17\u3002\u6771\u4eac4\u2103\u3063\u3066\u7d50\u69cb\u5bd2\u304f\u306a\u3044\u3058\u3083\u3093\u3002\uff08\u78ba\u304b\u306b\u3082\u306e\u51c4\u304f\u5bd2\u3044\u8a33\u3067\u306f\u7121\u3044\uff09\u3002\u798f\u5ca1\u5e02\u306f1\u2103\u3060\u3002\u52dd\u3061\u3060\u305c\u3002Rock'n'roll","id":30665765698928640,"from_user_id":103259265,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"165242761","profile_image_url":"http://a1.twimg.com/profile_images/1147334744/SN3E00600001_normal.jpg","created_at":"Thu, 27 Jan 2011 16:37:35 +0000","from_user":"deuxavril0502","id_str":"30665736556912640","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6b8b\u5ff5\u3001\u79c1\u306f\u6c96\u7e04\u3067\u3059(&gt;_&lt;)\u3067\u3082\u304a\u5929\u6c17\u826f\u3055\u305d\u3046\u3067\u826f\u304b\u3063\u305f\u306d\u3002\u3053\u3061\u3089\u306f\u96e8\u3088\uff5eRT @rinandy2010: \u305d\u3046\u3067\u3059\u3063\u266a\u51fa\u5f35\u3067(^^)\u304a\u306d\u3048\u3055\u307e\u306f\u6771\u4eac\u3067\u3059\u304b\uff1f\u3081\u3063\u3061\u3083\u5929\u6c17\u826f\u304f\u3066\u3073\u3063\u304f\u308a\u3067\u3059\u3002\u624b\u888b\u3068\u304b\u5168\u7136\u3044\u3089\u306a\u3044\u3067\u3059\u306d\u30fc RT @deuxavril0502 \u6771\u4eac\u306a\u306e\u30fc\uff1f","id":30665736556912640,"from_user_id":165242761,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.echofon.com/&quot; rel=&quot;nofollow&quot;&gt;Echofon&lt;/a&gt;"},{"from_user_id_str":"163900288","profile_image_url":"http://a1.twimg.com/profile_images/1210224823/icon12945064955238_normal.jpg","created_at":"Thu, 27 Jan 2011 16:24:37 +0000","from_user":"nyao_yurichan","id_str":"30662472734089216","metadata":{"result_type":"recent"},"to_user_id":100985873,"text":"@ray_ko302 \u767a\u898b\u3042\u308a\u304c\u3068\u3067\u3059\u3045\u3002\u3053\u3061\u3089\u3067\u3082\u3088\u308d\u3057\u304f\u306d\u3002\u6771\u4eac\u306f\u4eca\u65e5\u3082\u4e7e\u71e5\u3067\u5927\u5909\u3088\u3002\u305d\u3063\u3061\u3068\u771f\u9006\u306e\u5929\u6c17\u3060\u306d\u3002\u30a2\u30a4\u30b9\u30d0\u30fc\u30f3\u304d\u3092\u3064\u3051\u3066\u3088\u306d\u3002","id":30662472734089216,"from_user_id":163900288,"to_user":"ray_ko302","geo":null,"iso_language_code":"ja","to_user_id_str":"100985873","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"49227098","profile_image_url":"http://a3.twimg.com/profile_images/965007882/____normal.jpg","created_at":"Thu, 27 Jan 2011 15:52:26 +0000","from_user":"dosannko6","id_str":"30654371016478720","metadata":{"result_type":"recent"},"to_user_id":108570870,"text":"@peke_hajiP \u4ffa\u304c\u6771\u4eac \u6765\u3066\u6700\u521d\u306b\u9a5a\u3044\u305f\u306e\u304c\u3001\u5929\u6c17\u4e88\u5831\u3067\u82b1\u7c89\u60c5\u5831\u304c\u6d41\u308c\u308b\u3053\u3068\u3067\u3001\u6700\u521d\u306b\u8a66\u3057\u305f\u306e\u304c \u30b4\u30ad\u69d8 \u53ec\u559a\u306e\u5100\u5f0f\u3067\u3059\u304a\uff1f","id":30654371016478720,"from_user_id":49227098,"to_user":"peke_hajip","geo":null,"iso_language_code":"ja","to_user_id_str":"108570870","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"71148934","profile_image_url":"http://a0.twimg.com/profile_images/1209223630/image_normal.jpg","created_at":"Thu, 27 Jan 2011 15:46:49 +0000","from_user":"123keiko","id_str":"30652958311981058","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u9727\u5cf6\u306e\u964d\u7070\u306e\u30cb\u30e5\u30fc\u30b9\u3092\u898b\u3066\u601d\u3044\u51fa\u3057\u307e\u3057\u305f\u3002\u4e0a\u4eac\u3057\u305f\u59cb\u3081\u306e\u9803\u3001\u6771\u4eac\u306f1\u5e74\u4e2d\u3001\u7070\u304c\u964d\u3089\u306a\u3044\u304b\u3089\u7a7a\u6c17\u304c\u6f84\u3093\u3067\u3066\u904e\u3054\u3057\u3084\u3059\u3044\u306a\u3041\u3001\u3068\u601d\u3063\u305f\u306a\u3041\u3001\u3063\u3066\u3002\n\u5b9f\u5bb6\u306f\u51ac\u306e\u5b63\u7bc0\u98a8\u3067\u685c\u5cf6\u306e\u7070\u304c\u964d\u308b\u5730\u533a\u3067\u3057\u305f\u3002\u9e7f\u5150\u5cf6\u306e\u5929\u6c17\u4e88\u5831\u3067\u306f\u3001\u6bce\u65e5\u3001\u685c\u5cf6\u4e0a\u7a7a\u306e\u98a8\u5411\u304d\u4e88\u5831\u304c\u3067\u307e\u3059\u3002\u3053\u308c\u304b\u3089\u306f\u9727\u5cf6\u3082\u304b\u306a\uff1f","id":30652958311981058,"from_user_id":71148934,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"91124773","profile_image_url":"http://a3.twimg.com/profile_images/1139308356/prof101007_3-1_normal.jpg","created_at":"Thu, 27 Jan 2011 15:43:35 +0000","from_user":"sanposuruhito","id_str":"30652146277945345","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u4eca\u5bb5\u306e\u90fd\u5fc3\u306f\u3001\u8eab\u3082\u5f15\u304d\u7de0\u307e\u308b\u3068\u3044\u3046\u3088\u308a\u306f\u51cd\u3048\u308b\u7a0b\u306e\u51b7\u6c17\u3092\u611f\u3058\u308b\u5bd2\u3044\u591c\u3067\u3059\u3002\u660e\u65e5\u65e5\u4e2d\u306e\u6771\u4eac\u306e\u5929\u6c17\u306f\u3001\u5915\u65b9\u306b\u591a\u5c11\u96f2\u304c\u51fa\u308b\u5834\u6240\u304c\u3042\u308a\u305d\u3046\u306a\u3082\u306e\u306e\u6982\u306d\u6674\u308c\u7a7a\u304c\u5e83\u304c\u308a\u7d9a\u3051\u305d\u3046\u3067\u3059\u3002\u6700\u9ad8\u6c17\u6e29\u306f\u30015-6\u5ea6\u4f4d\u3068\u306a\u308a\u305d\u3046\u3067\u3059\u3002#weather_tokyo","id":30652146277945345,"from_user_id":91124773,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://projects.playwell.jp/go/Saezuri&quot; rel=&quot;nofollow&quot;&gt;Saezuri&lt;/a&gt;"},{"from_user_id_str":"104712480","profile_image_url":"http://a3.twimg.com/profile_images/1204016928/icon5087697006727791368kuro2_normal.jpg","created_at":"Thu, 27 Jan 2011 15:31:51 +0000","from_user":"asongfor_xx","id_str":"30649192296751104","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u7f8e\u4eba\u5929\u6c17\u304b\u3089tweet\u3059\u308b\u3068\u3053\u3093\u306a\u611f\u3058\u306a\u3093\u3060\u306a\u3002 RT asongfor_xx: \u3010\u7f8e\u4eba\u5929\u6c17/\u6771\u4eac\u3011\u7f8e\u4eba\u5929\u6c17\u30ad\u30e3\u30b9\u30bf\u30fc\u306e&quot;\u304b\u306a\u3053\u3055\u3093&quot;\u306b\u3088\u308b\u3068\u300c1/28(\u91d1)\u306f\u6674\u6642\u3005\u304f\u3082\u308a\u3067\u3001\u964d\u6c34\u78ba\u73870%\u3001\u6700\u9ad8\u6c17\u6e29\u306f8\u2103\u3067\u6700\u4f4e\u6c17\u6e29\u306f1\u2103\u3067\u3059\u300d\u7f8e\u4eba\u5929\u6c17\u21d2http://bit.ly/djB8th","id":30649192296751104,"from_user_id":104712480,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twipple.jp/&quot; rel=&quot;nofollow&quot;&gt;\u3064\u3044\u3063\u3077\u308b for iPhone&lt;/a&gt;"},{"from_user_id_str":"165965113","profile_image_url":"http://a0.twimg.com/profile_images/1200885316/shogomeguro_normal.jpg","created_at":"Thu, 27 Jan 2011 15:24:35 +0000","from_user":"shogomeguro","id_str":"30647362477105152","metadata":{"result_type":"recent"},"to_user_id":null,"text":"2011.1.26 \u793e\u7a93\u304b\u3089\u3002\u4e0d\u601d\u8b70\u306a\u5929\u6c17\u3067\u3053\u306e\u5f8c\u96ea\u304c\u3061\u3089\u3064\u3044\u305f\u3093\u3060\u3088\u306d\u3002\u6771\u4eac\u306e\u521d\u96ea\u3067\u3057\u305f\u3002 http://instagr.am/p/BO_xn/","id":30647362477105152,"from_user_id":165965113,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://instagr.am&quot; rel=&quot;nofollow&quot;&gt;instagram&lt;/a&gt;"},{"from_user_id_str":"166018520","profile_image_url":"http://a0.twimg.com/profile_images/1207460953/100-40030_normal.jpg","created_at":"Thu, 27 Jan 2011 15:18:17 +0000","from_user":"115nayume","id_str":"30645776254238722","metadata":{"result_type":"recent"},"to_user_id":118384679,"text":"@dancinpea09 \u6771\u4eac\u3001\u660e\u65e5\u3082\u5929\u6c17\u826f\u3044\u307f\u305f\u3044\u3067\u3001\u3088\u304b\u3063\u305f\u3067\u3059\u306d\u30fc\u266a  \u30ca\u30a4\u30b9\u30c8\u30ea\u30c3\u30d7\uff01\u304a\u3084\u3059\u307f\u306a\u3055\u3044\u3002 http://twitpic.com/3tvun3","id":30645776254238722,"from_user_id":166018520,"to_user":"dancinpea09","geo":null,"iso_language_code":"ja","to_user_id_str":"118384679","source":"&lt;a href=&quot;http://tweetli.st/&quot; rel=&quot;nofollow&quot;&gt;TweetList!&lt;/a&gt;"},{"from_user_id_str":"202937674","profile_image_url":"http://a1.twimg.com/profile_images/1218797578/______2_normal.jpg","created_at":"Thu, 27 Jan 2011 15:11:48 +0000","from_user":"ryuusisan","id_str":"30644146498699264","metadata":{"result_type":"recent"},"to_user_id":203058384,"text":"@manaasutakshi \u6771\u4eac\u306f\u964d\u3063\u3066\u306a\u3044\u3093\u3067\u3059\u304b\u30fb\u30fb\u30fb\u5929\u6c17\u4e88\u5831\u306b\u3088\u308b\u3068\u660e\u65e5\u3082\u6771\u4eac\u306f\u964d\u3089\u306a\u3044\u305d\u3046\u3067\u3059\u306d\uff01\uff01\u79c1\u306e\u4f4f\u3093\u3067\u3044\u308b\u3068\u3053\u308d\u306f\u5fae\u5999\u306b\u964d\u308a\u305d\u3046\u3067\u51fa\u52e4\u6642\u306b\u964d\u3063\u3066\u305f\u3089\u3061\u3087\u3063\u3068\u5acc\u3060\u306a\u3041\u3000\u306a\u3093\u3066\u601d\u3044\u307e\u3059","id":30644146498699264,"from_user_id":202937674,"to_user":"manaasutakshi","geo":null,"iso_language_code":"ja","to_user_id_str":"203058384","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"3222182","profile_image_url":"http://a0.twimg.com/profile_images/1199090957/ProfilePhoto_normal.png","created_at":"Thu, 27 Jan 2011 15:09:35 +0000","from_user":"44mune","id_str":"30643589545459713","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3010\u7f8e\u4eba\u5929\u6c17/\u6771\u4eac\u3011\u7f8e\u4eba\u5929\u6c17\u30ad\u30e3\u30b9\u30bf\u30fc\u306e&quot;\u306e\u308a\u3055\u3093&quot;\u306b\u3088\u308b\u3068\u300c1/28(\u91d1)\u306f\u304f\u3082\u308a\u306e\u3061\u6674\u3067\u3001\u964d\u6c34\u78ba\u738710%\u3001\u6700\u9ad8\u6c17\u6e29\u306f9\u2103\u3067\u6700\u4f4e\u6c17\u6e29\u306f2\u2103\u3067\u3059\u300d http://twitpic.com/3tvruh","id":30643589545459713,"from_user_id":3222182,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://bijintenki.jp&quot; rel=&quot;nofollow&quot;&gt;bijintenki.jp&lt;/a&gt;"},{"from_user_id_str":"154341982","profile_image_url":"http://a0.twimg.com/profile_images/1155496864/_____normal.jpg","created_at":"Thu, 27 Jan 2011 15:02:39 +0000","from_user":"slave_420_bot","id_str":"30641845490946049","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u4eca\u30f3\u3068\u3053\u899a\u3048\u3066\u308b\u306e\u306f\u300c\u672d\u5e4c\u300d\u300c\u4ed9\u53f0\u300d\u300c\u6771\u4eac\u300d\u300c\u5927\u962a\u300d\u300c\u540d\u53e4\u5c4b\u300d\u300c\u798f\u5ca1\u300d\u306e\u4eca\u65e5\u3068\u660e\u65e5\u306e\u5929\u6c17\u3060\u306a\u3002\u5929\u6c17\u304c\u77e5\u308a\u305f\u3044\u6642\u306f\u300c\u5834\u6240\u300d\u3068\u300c\u4eca\u65e5\u306e\u5929\u6c17\u300d\u304b\u300c\u660e\u65e5\u306e\u5929\u6c17\u300d\u3092\u304a\u7533\u3057\u4ed8\u3051\u304f\u3060\u3055\u3044\u3001\u304a\u5b22\u69d8\uff1f","id":30641845490946049,"from_user_id":154341982,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://bot.syoyu.net/&quot; rel=&quot;nofollow&quot;&gt;\u7adc\u30f6\u5cf0\u90b8\u306e\u3069\u3053\u304b&lt;/a&gt;"},{"from_user_id_str":"72042678","profile_image_url":"http://a3.twimg.com/profile_images/1198771027/larxene_normal.jpg","created_at":"Thu, 27 Jan 2011 15:01:57 +0000","from_user":"larxene_bot","id_str":"30641668772339712","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u4eca\u65e5\u306e\u5929\u6c17\u306f \u672d\u5e4c\u304c\u6674\u308c \u4ed9\u53f0\u304c\u66c7\u6642\u3005\u96ea \u6771\u4eac\u304c\u6674\u306e\u3061\u66c7 \u540d\u53e4\u5c4b\u304c\u6674\u6642\u3005\u66c7 \u5927\u962a\u304c\u6674\u6642\u3005\u66c7 \u798f\u5ca1\u304c\u6674\u308c \u3089\u3057\u3044\u308f\u3088 \u6ce8\u610f\u3057\u306a\u3055\u3044","id":30641668772339712,"from_user_id":72042678,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www4.kiwi-us.com/~yuuna/larxene_bot_manual/index.html&quot; rel=&quot;nofollow&quot;&gt;\u5fd8\u5374\u306e\u57ce \u5730\u4e0a\u306e12\u968e&lt;/a&gt;"},{"from_user_id_str":"101331893","profile_image_url":"http://a1.twimg.com/sticky/default_profile_images/default_profile_0_normal.png","created_at":"Thu, 27 Jan 2011 15:01:34 +0000","from_user":"yoshiftan","id_str":"30641572240424961","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6771\u4eac\u306e\u5929\u6c17\u4e88\u5831\u3092\u898b\u3066\u3073\u3063\u304f\u308a\uff01\u6691\u305d\u3046\u3060\u306a\u3053\u308a\u3083\u3002","id":30641572240424961,"from_user_id":101331893,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"Keitai Mail"},{"from_user_id_str":"65207550","profile_image_url":"http://a3.twimg.com/profile_images/448731969/_________R_normal.jpg","created_at":"Thu, 27 Jan 2011 15:01:33 +0000","from_user":"kabuyo","id_str":"30641566469066752","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u5929\u6c17\u4e88\u5831\u3067\u306f\u6771\u4eac\u3067\u306f\u4e7e\u71e5\u6ce8\u610f\u5831\u304c\u7d9a\u3044\u3066\u3044\u308b\u3089\u3057\u3044\u306e\u306b\u3001\u306a\u305c\u79c1\u304c\u5e72\u3057\u305f\u6d17\u6fef\u7269\u306f\u96e8\u306b\u6fe1\u308c\u3066\u3057\u307e\u3063\u305f\u306e\u3060\u308d\u3046\u304b\u2025\u3000\u3088\u3063\u307d\u3069\u904b\u304c\u60aa\u3044\u3089\u3057\u3044\u2025","id":30641566469066752,"from_user_id":65207550,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"80309352","profile_image_url":"http://a1.twimg.com/profile_images/1223620418/image_normal.jpg","created_at":"Thu, 27 Jan 2011 14:54:30 +0000","from_user":"mikutyan","id_str":"30639792026812416","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6771\u4eac\u306e\u660e\u65e5\u306e\u5929\u6c17\u6559\u3048\u3066","id":30639792026812416,"from_user_id":80309352,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"80309352","profile_image_url":"http://a1.twimg.com/profile_images/1223620418/image_normal.jpg","created_at":"Thu, 27 Jan 2011 14:53:06 +0000","from_user":"mikutyan","id_str":"30639439847890944","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u660e\u65e5\u306e\u6771\u4eac\u306e\u5929\u6c17\u306a\u3093\u3060\u308d\u3046 \u96e8\u964d\u3063\u305f\u3089\u56f0\u308b\u3002","id":30639439847890944,"from_user_id":80309352,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"120911929","profile_image_url":"http://a0.twimg.com/profile_images/772435076/20100312___capture2_normal.png","created_at":"Thu, 27 Jan 2011 14:47:19 +0000","from_user":"Cirno_fan","id_str":"30637984684449792","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3042\u305f\u3044\u30c1\u30eb\u30ce\uff01\u30dc\u30c3\u30c8\u3060\u3051\u3069\u5929\u6c17\u4e88\u5831\u3082\u3067\u304d\u308b\u3088\uff01\u300c\u6771\u4eac\u306e\u4eca\u65e5\u306e\u5929\u6c17\u6559\u3048\u3066\u300d\u307f\u305f\u3044\u306bTL\u767a\u8a00\u3057\u3066\u304f\u308c\u308c\u3070\u8abf\u3079\u3066\u3042\u3052\u308b\uff01 #followme #followmejp #followmevip #tenki","id":30637984684449792,"from_user_id":120911929,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://blog.livedoor.jp/fairycirno/archives/34686.html&quot; rel=&quot;nofollow&quot;&gt;\u5e7b\u60f3\u90f7 \u9727\u306e\u6e56&lt;/a&gt;"},{"from_user_id_str":"33858777","profile_image_url":"http://a0.twimg.com/profile_images/352864695/muga_normal.jpg","created_at":"Thu, 27 Jan 2011 14:46:12 +0000","from_user":"collonist","id_str":"30637702105796610","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6771\u4eac\u3063\u3066\u51ac\u306f\u57fa\u672c\u7684\u306b\u6674\u308c\u3066\u308b\u304b\u3089\u3001\u5929\u6c17\u4e88\u5831\u898b\u306a\u304f\u3066\u3082\u5168\u7136\u554f\u984c\u306a\u3044\u3002\u5c71\u5f62\u3060\u3068\u660e\u65e5\u306f\u3069\u3093\u3060\u3051\u96ea\u304c\u964d\u308b\u306e\u304b\u3001\u3069\u3093\u3060\u3051\u5bd2\u3044\u306e\u304b\u628a\u63e1\u3057\u3066\u3068\u304d\u305f\u3044\u306e\u3067\u3001\u5929\u6c17\u4e88\u5831\u306f\u5fc5\u305a\u30c1\u30a7\u30c3\u30af\u3057\u3066\u305f\u306e\u3060\u304c\u3002","id":30637702105796610,"from_user_id":33858777,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.hootsuite.com&quot; rel=&quot;nofollow&quot;&gt;HootSuite&lt;/a&gt;"},{"from_user_id_str":"79336228","profile_image_url":"http://a0.twimg.com/profile_images/1130029876/zmanmu_normal.png","created_at":"Thu, 27 Jan 2011 14:30:23 +0000","from_user":"atsuki777","id_str":"30633725041573890","metadata":{"result_type":"recent"},"to_user_id":null,"text":"MXTV\u306e\u5929\u6c17\u4e88\u5831\u521d\u3081\u3066\u898b\u305f\u3051\u3069\u6771\u4eac\u753a\u7530\u516b\u738b\u5b50\u3063\u3066\u5730\u57df\u8868\u793a\u304c\u2026w\u516b\u738b\u5b50\u3068\u753a\u7530\u306f\u6771\u4eac\u3058\u3083\u306a\u3044\u306e\u304bwww","id":30633725041573890,"from_user_id":79336228,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.echofon.com/&quot; rel=&quot;nofollow&quot;&gt;Echofon&lt;/a&gt;"},{"from_user_id_str":"90576097","profile_image_url":"http://a0.twimg.com/profile_images/1203320254/nanami1_normal.png","created_at":"Thu, 27 Jan 2011 14:28:26 +0000","from_user":"dycroft","id_str":"30633234245099521","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6771\u4eac\u306e\u660e\u65e5\u306e\u304a\u5929\u6c17\u3067\u3059","id":30633234245099521,"from_user_id":90576097,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://sourceforge.jp/projects/tween/wiki/FrontPage&quot; rel=&quot;nofollow&quot;&gt;Tween&lt;/a&gt;"},{"from_user_id_str":"147215268","profile_image_url":"http://a2.twimg.com/profile_images/1198555202/ichika-7.4.0-3P_normal.gif","created_at":"Thu, 27 Jan 2011 14:28:23 +0000","from_user":"kiaran5032","id_str":"30633222211637248","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6771\u4eac\u306e\u660e\u65e5\u306e\u304a\u5929\u6c17\u3067\u3059\u3002\u6674\u308c\u6642\u3005\u304f\u3082\u308a #MX","id":30633222211637248,"from_user_id":147215268,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://sourceforge.jp/projects/tween/wiki/FrontPage&quot; rel=&quot;nofollow&quot;&gt;Tween&lt;/a&gt;"},{"from_user_id_str":"1741599","profile_image_url":"http://a0.twimg.com/profile_images/1205744888/950637_normal.jpg","created_at":"Thu, 27 Jan 2011 14:28:21 +0000","from_user":"dasaitama","id_str":"30633212535382016","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6771\u4eac\u306e\u3001\u660e\u65e5\u306e\u304a\u5929\u6c17\u3067\u3059","id":30633212535382016,"from_user_id":1741599,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://sourceforge.jp/projects/tween/wiki/FrontPage&quot; rel=&quot;nofollow&quot;&gt;Tween&lt;/a&gt;"},{"from_user_id_str":"34140879","profile_image_url":"http://a3.twimg.com/profile_images/1186413535/__6_normal.jpg","created_at":"Thu, 27 Jan 2011 14:28:17 +0000","from_user":"ryuji_chaos","id_str":"30633197016449024","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6771\u4eac\u306e\u660e\u65e5\u306e\u304a\u5929\u6c17\u3067\u3059\u3002","id":30633197016449024,"from_user_id":34140879,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"1400","profile_image_url":"http://a0.twimg.com/profile_images/1198556839/westerndog_20090104_320x320_normal.jpg","created_at":"Thu, 27 Jan 2011 14:28:16 +0000","from_user":"westerndog","id_str":"30633191232503808","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6771\u4eac\u306e\u660e\u65e5\u306e\u304a\u5929\u6c17\u3067\u3059","id":30633191232503808,"from_user_id":1400,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://sourceforge.jp/projects/tween/wiki/FrontPage&quot; rel=&quot;nofollow&quot;&gt;Tween&lt;/a&gt;"},{"from_user_id_str":"89239832","profile_image_url":"http://a3.twimg.com/profile_images/519522846/noriko_normal.jpg","created_at":"Thu, 27 Jan 2011 14:15:01 +0000","from_user":"Noriko_Fujimoto","id_str":"30629857234788352","metadata":{"result_type":"recent"},"to_user_id":115645219,"text":"@nossa430402 \u5bcc\u5c71\u306f\u3059\u3054\u3044\u96ea\u307f\u305f\u3044\u3067\u3059\u306d\uff5e\uff08\u5bd2\u3063\uff09\u3000\u6771\u4eac\u306f\u305d\u306e\u5206\u6bce\u65e5\u826f\u3044\u5929\u6c17\u3067\u3059\u3002\u5bd2\u3044\u3051\u3069\u30fb\u30fb\u30fb\u3002","id":30629857234788352,"from_user_id":89239832,"to_user":"nossa430402","geo":null,"iso_language_code":"ja","to_user_id_str":"115645219","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"107265539","profile_image_url":"http://a0.twimg.com/sticky/default_profile_images/default_profile_3_normal.png","created_at":"Thu, 27 Jan 2011 14:01:10 +0000","from_user":"kurumasuki330","id_str":"30626371826884608","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u4eca\u9031\u571f\u66dc\u65e5\u3001\u8eca\u3067\u6771\u4eac\u304b\u3089\u6771\u540d\u3001\u7c73\u539f\u7d4c\u7531\u3067\u91d1\u6ca2\u306e\u5b9f\u5bb6\u3078\u3002\u305d\u3057\u3066\u3001\u6708\u66dc\u65e5\u306b\u6771\u4eac\u3078\u5e30\u308b\u30b9\u30b1\u30b8\u30e5\u30fc\u30eb\u3002\u5929\u6c17\u4e88\u5831\u3060\u3068\u4eca\u9031\u672b\u3001\u5317\u9678\u5730\u65b9\u306f\u5927\u96ea\uff01\u5e30\u308c\u308b\u304b\u81ea\u4fe1\u304c\u306a\u304f\u306a\u3063\u3066\u304d\u305f\u3002\u307e\u305f\u3001\u305d\u306e\u6642\u5b9f\u6cc1\u4e2d\u7d99\u3057\u307e\u3059\u3002","id":30626371826884608,"from_user_id":107265539,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twtr.jp&quot; rel=&quot;nofollow&quot;&gt;Keitai Web&lt;/a&gt;"},{"from_user_id_str":"155313166","profile_image_url":"http://a3.twimg.com/profile_images/1167554721/73931_1408370184003_1674923704_920888_5686998_n_normal.jpg","created_at":"Thu, 27 Jan 2011 14:00:50 +0000","from_user":"takuya_aoki","id_str":"30626287127105536","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6771\u4eac\u306f\u3081\u3063\u3061\u3083\u3044\u3044\u5929\u6c17\u3067\u904e\u3054\u3057\u3084\u3059\u3044\u3051\u3069\u30c9\u30e9\u30a4\u30a2\u30a4\u306b\u306f\u3061\u3087\u3063\u3068\u304d\u3064\u3044\u306a\u30fb\u30fb\u30fb\u76ee\u85ac\u304c\u624b\u653e\u305b\u306a\u3044","id":30626287127105536,"from_user_id":155313166,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"179984398","profile_image_url":"http://a1.twimg.com/profile_images/1182382310/g4350_normal.jpg","created_at":"Thu, 27 Jan 2011 13:52:50 +0000","from_user":"aiilabo","id_str":"30624271902449664","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u30d6\u30ed\u30b0\u66f4\u65b0 \u25c6\u611f\u60c5\u3092\u307f\u3064\u3081\u308b\u65b9\u6cd5 - \u3053\u3053\u6570\u65e5\u3001\u6642\u6298\u96e8\u304c\u30d1\u30e9\u30d1\u30e9\u3000\u5fae\u5999\u306a\u5929\u6c17\u306e\u6771\u4eac\u304b\u3089\u3042\u308b\u304c\u307e\u307e\u3067\u3059\u3002 \n\n \u4eca\u65e5\u306f\u3001\u6628\u65e5\u306e\u300c\u30de\u30a4\u30ca\u30b9\u611f\u60c5\u306f\u60aa\u3067\u3059\u304b\uff1f\n.. \u27aa http://am6.jp/e3e9tS","id":30624271902449664,"from_user_id":179984398,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://feedtweet.am6.jp/&quot; rel=&quot;nofollow&quot;&gt;\u261e feedtweet.jp \u261c&lt;/a&gt;"},{"from_user_id_str":"185992391","profile_image_url":"http://a2.twimg.com/profile_images/1192695844/otenki_normal.png","created_at":"Thu, 27 Jan 2011 13:00:37 +0000","from_user":"otenki_luci_bot","id_str":"30611130938298368","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u767a\u58f2\u65e5\u304b\u2026\u2026\u541b\u305f\u3061\u306b\u3068\u3063\u3066\u306f\u591a\u5206\u300190\u65e5\u5f8c\u306e\u3053\u3068\u3060\u3002\u3055\u3066\u660e\u65e5\u306e\u6771\u4eac\u90fd\u306e\u5929\u6c17\u306f\u6674\u306e\u3061\u66c7\u3001\u6700\u9ad8\u6c17\u6e29\u306f8\u5ea6\u3060\u305d\u3046\u3060\u3002\u3055\u3059\u304c\u306b\u4f55\u304b\u7740\u305f\u65b9\u304c\u3044\u3044\u305e\u3001\u30a4\u30fc\u30ce\u30c3\u30af\u3002","id":30611130938298368,"from_user_id":185992391,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/otenki_luci_bot&quot; rel=&quot;nofollow&quot;&gt;\u304a\u3066\u3093\u304d\u308b\u3057\u3075\u3047\u308b&lt;/a&gt;"},{"from_user_id_str":"65976527","profile_image_url":"http://a0.twimg.com/profile_images/452810990/little_italies_____normal.jpg","created_at":"Thu, 27 Jan 2011 13:00:04 +0000","from_user":"heta_weather01","id_str":"30610995843964928","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3010\u30d8\u30bf\u5929\u3011Jo napot.\u30cf\u30f3\u30ac\u30ea\u30fc\u3067\u3059\u3002\u6771\u4eac\u306e\u5929\u6c17\u306f\u6674\u306e\u3061\u66c7\u3067\u660e\u65e5\u306f\u6674\u6642\u3005\u66c7\u3001\u79c1\u306e\u3068\u3053\u308d\u3067\u306f\u66c7\u308a\u3067-2\u2103\u3067\u3059\u3002\u3048\u3001\u3053\u306e\u30d5\u30e9\u30a4\u30d1\u30f3\u3067\u3059\u304b\uff1f\u2026\u3061\u3087\u3063\u3068\u500b\u4eba\u7684\u306b\u6bb4\u308a\u305f\u3044\u4eba\u304c\u3044\u308b\u3093\u3067\u3059\u266a","id":30610995843964928,"from_user_id":65976527,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www15.atpages.jp/~kageyanma/&quot; rel=&quot;nofollow&quot;&gt;\u5730\u7403\u306e\u4e2d&lt;/a&gt;"},{"from_user_id_str":"109740077","profile_image_url":"http://a0.twimg.com/profile_images/1212763349/1d3d3821-6b3a-449f-a636-05b95ce3d5d8_normal.png","created_at":"Thu, 27 Jan 2011 12:59:41 +0000","from_user":"snow_arai","id_str":"30610896141156354","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u6771\u4eac\u306f\u3001\u30ab\u30e9\u30ab\u30e9\u9023\u7d9a\u3000\uff13\u4f4d\u30bf\u30a4 - \u65e9\u8d77\u304d\u2606\u304a\u5929\u6c17\u2606ONAIR\u65e5\u8a18\uff1a\u65e5\u7d4c\u30a6\u30fc\u30de\u30f3\u30aa\u30f3\u30e9\u30a4\u30f3 http://j.mp/hxP3Di","id":30610896141156354,"from_user_id":109740077,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"53407951","profile_image_url":"http://a1.twimg.com/profile_images/397960795/airman_normal.jpg","created_at":"Thu, 27 Jan 2011 12:57:32 +0000","from_user":"Airman2009","id_str":"30610355541508096","metadata":{"result_type":"recent"},"to_user_id":null,"text":". @04mmy22 @qsk1 \u98a8\u3068\u96f2\u30fb\u30fb\u30fb\u98db\u884c\u6a5f\u306e\u5927\u5c0f\u554f\u308f\u305a\u3001\u3053\u308c\u3070\u304b\u308a\u306f\u3069\u3046\u3057\u3088\u3046\u3082\u7121\u3044\u3067\u3059\u3088\u306d\u3002 \u6700\u8fd1\u306e\u5357\u6771\u4eac\u306e\u5929\u6c17\u306f\u5909\u3067\u3059\u306d\u3002\u3002\u3002","id":30610355541508096,"from_user_id":53407951,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://sourceforge.jp/projects/tween/wiki/FrontPage&quot; rel=&quot;nofollow&quot;&gt;Tween&lt;/a&gt;"},{"from_user_id_str":"111826019","profile_image_url":"http://a1.twimg.com/profile_images/1121742732/DVC00052_normal.jpg","created_at":"Thu, 27 Jan 2011 12:51:12 +0000","from_user":"nijiks","id_str":"30608762859421696","metadata":{"result_type":"recent"},"to_user_id":100014282,"text":"@sanaeru  \u79c1\u3053\u3093\u306a\u5929\u6c17\u306e\u4e2d\uff64\u4e8c\u6cca\u4e09\u65e5\u3067\u660e\u65e5\u304b\u3089\u6771\u4eac\u884c\u304f\u3093\u3067\u3059\u3051\u3069\u2026\uff61\u65e5\u66dc\u65e5\u98db\u884c\u6a5f\u98db\u3076\u304b\u3057\u3089\u2026\uff1f","id":30608762859421696,"from_user_id":111826019,"to_user":"sanaeru","geo":null,"iso_language_code":"ja","to_user_id_str":"100014282","source":"&lt;a href=&quot;http://z.twipple.jp/&quot; rel=&quot;nofollow&quot;&gt;\u3064\u3044\u3063\u3077\u308b/twipple&lt;/a&gt;"},{"from_user_id_str":"164951488","profile_image_url":"http://a3.twimg.com/profile_images/799082230/twitter_normal.jpg","created_at":"Thu, 27 Jan 2011 12:47:49 +0000","from_user":"minorif_jp","id_str":"30607912309096448","metadata":{"result_type":"recent"},"to_user_id":105078670,"text":"@ecco_HIRADO \u5e73\u6238\u725b\u3063\u3066\u5143\u306f\u30aa\u30e9\u30f3\u30c0\u304b\u3089\u304d\u305f\u725b\u306a\u3093\u3060\u306d\u3002\u304a\u3044\u3057\u305d\u3046\u2026\u3002\u5e73\u6238\u306f\u3088\u304b\u6240\u305f\u3043\u3002\u3042\u305f\u3044\u3052\u3047\u3093\u5b9f\u5bb6\u3093\u65b9\u306f\u30c0\u30d6\u30eb\u706b\u5c71\u3084\u9ce5\u30a4\u30f3\u30d5\u30eb\u3067\u5927\u5909\u3084\u3063\u3069\u3002\u6771\u4eac\u3093\u826f\u304b\u3068\u3053\u306f\u5929\u6c17\u306e\u5fc3\u914d\u305b\u305a\u66ae\u3089\u305b\u308b\u3068\u3053\u3058\u3083\u3063\u3069\u3093\u3002","id":30607912309096448,"from_user_id":164951488,"to_user":"ecco_HIRADO","geo":null,"iso_language_code":"ja","to_user_id_str":"105078670","source":"&lt;a href=&quot;http://z.twipple.jp/&quot; rel=&quot;nofollow&quot;&gt;\u3064\u3044\u3063\u3077\u308b/twipple&lt;/a&gt;"},{"from_user_id_str":"10456803","profile_image_url":"http://a1.twimg.com/profile_images/145280215/vot_icon_normal.png","created_at":"Thu, 27 Jan 2011 12:43:37 +0000","from_user":"vot_","id_str":"30606853377363968","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3086\u3063\u304f\u308a\u5929\u6c17\u4e88\u5831\u3067\u3059\u3002\u6771\u4eac\u90fd,\u5927\u5cf6\u5730\u65b9\u300228\u65e5\uff08\u91d1\u66dc\u65e5\u306e\u5929\u6c17\u306f\u6674\u308c\u306e\u3061\u66c7\u308a\u3001\u6700\u9ad8\u6c17\u6e29\u306f7\u5ea6 \u6700\u4f4e\u6c17\u6e29\u306f1\u5ea6\u3067\u3057\u3087\u3046\u3002","id":30606853377363968,"from_user_id":10456803,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://voiceoftwitter.com&quot; rel=&quot;nofollow&quot;&gt;\uff56\uff4f\uff49\uff43\uff45 \uff4f\uff46 \uff54\uff57\uff49\uff54\uff54\uff45\uff52&lt;/a&gt;"},{"from_user_id_str":"23900716","profile_image_url":"http://a3.twimg.com/profile_images/1092646332/twitter_icon_normal.png","created_at":"Thu, 27 Jan 2011 12:38:00 +0000","from_user":"keiyama","id_str":"30605442639990784","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3010\u7f8e\u4eba\u5929\u6c17/\u6771\u4eac\u3011\u7f8e\u4eba\u5929\u6c17\u30ad\u30e3\u30b9\u30bf\u30fc\u306e&quot;\u30c8\u30e2\u30b3\u3055\u3093&quot;\u306b\u3088\u308b\u3068\u300c1/27(\u6728)\u306f\u304f\u3082\u308a\u306e\u3061\u6674\u3067\u3001\u964d\u6c34\u78ba\u738710%\u3001\u6700\u9ad8\u6c17\u6e29\u306f9\u2103\u3067\u6700\u4f4e\u6c17\u6e29\u306f2\u2103\u3067\u3059\u300d\u7f8e\u4eba\u5929\u6c17\u21d2http://bit.ly/djB8th http://twitpic.com/3tuekm #bt_tenki","id":30605442639990784,"from_user_id":23900716,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://bijintenki.jp&quot; rel=&quot;nofollow&quot;&gt;bijintenki.jp&lt;/a&gt;"},{"from_user_id_str":"32018516","profile_image_url":"http://a0.twimg.com/profile_images/1089472069/237_normal.jpg","created_at":"Thu, 27 Jan 2011 12:36:28 +0000","from_user":"DigJandG","id_str":"30605057460281344","metadata":{"result_type":"recent"},"to_user_id":196050661,"text":"@kosumoworld55 \u5929\u6c17\u4e88\u5831\u3092\u898b\u3066\u3044\u3064\u3082\u601d\u3046\u3093\u3060\u3002\u4f55\u3067\u6a2a\u6d5c\u3042\u305f\u308a\u306f\u3044\u3064\u3082\u597d\u3044\u5929\u6c17\u306a\u3093\u3060\u3002\u6771\u4eac\u3082\u3002\u3061\u306a\u307f\u306b\u516c\u9b5a\u306f\u3042\u3093\u307e\u308a\u597d\u304d\u3067\u306f\u3042\u308a\u307e\u305b\u3093\u3002\u306a\u3093\u304b\u81ed\u304f\u3063\u3066\u3002\u3067\u3082\u3001\u7435\u7436\u6e56\u3067\u516c\u9b5a\u3063\u3066\u3042\u307e\u308a\u805e\u304b\u306a\u3044\u69d8\u306a\u30fb\u30fb\u30fb\u3002\u4ffa\u304c\u91e3\u308a\u3092\u3057\u306a\u3044\u304b\u3089\u304b\u306a\uff1f","id":30605057460281344,"from_user_id":32018516,"to_user":"kosumoworld55","geo":null,"iso_language_code":"ja","to_user_id_str":"196050661","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"90935348","profile_image_url":"http://a1.twimg.com/sticky/default_profile_images/default_profile_0_normal.png","created_at":"Thu, 27 Jan 2011 12:34:34 +0000","from_user":"sorakeiko","id_str":"30604575937396737","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u95a2\u6771\u5730\u65b9\u306e\u660e\u65e5\u306e\u5929\u6c17\u2015\u7d9a\u304d\u3002\u660e\u65e5\u671d\u6700\u4f4e\u6c17\u6e29\u30fb\u8ed2\u4e26\u307f\u6c37\u70b9\u4e0b\u30fb\u5b87\u90fd\u5bae\u3001\u6c34\u6238\u30de\u30a4\u30ca\u30b95\u5ea6\u30fb\u6771\u4eac\u3001\u6a2a\u6d5c1\u5ea6\u3002\u6700\u9ad8\u6c17\u6e29\u30fb\u4eca\u65e5\u3088\u308a3\u5ea6\u524d\u5f8c\u4f4e\u304f5\uff5e8\u5ea6\u30fb\u6771\u4eac\u3001\u5343\u84498\u5ea6\u3002\u5411\u3053\u3046\u4e00\u9031\u9593\u30fb\u4e7e\u71e5\u3057\u305f\u6674\u308c\u7d9a\u304f\u30fb\uff08\u571f\uff09\u4f0a\u8c46\u8af8\u5cf6\u306b\u51b7\u305f\u3044\u96e8\u3084\u96ea\u304c\u898b\u3089\u308c\u305d\u3046\u3060\u304c\u3001\u95a2\u6771\u306e\u4e7e\u3044\u305f\u72b6\u614b\u7d9a\u304f\u3002","id":30604575937396737,"from_user_id":90935348,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"6113557","profile_image_url":"http://a3.twimg.com/profile_images/1227080026/profile_red_herencia_normal.png","created_at":"Thu, 27 Jan 2011 12:33:57 +0000","from_user":"fmoto7","id_str":"30604423327653888","metadata":{"result_type":"recent"},"to_user_id":null,"text":"&quot;\u6771\u4eac\u6c34\u904b\u7528\u30bb\u30f3\u30bf\u30fc\u306f\u3001\u524d\u534a\u6226\u304c\u7d42\u308f\u3063\u305f\u3068\u3053\u308d\u3067\u4e00\u6c17\u306b\u6c34\u5727\u9ad8\u3081\u3001\u5f8c\u534a\u6226\u59cb\u307e\u308b\u3068\u6c34\u5727\u4e0b\u3052\u3001\u7d42\u308f\u308b\u3068\u307e\u305f\u3059\u3050\u4e0a\u3052\u308b\u3002\u30c6\u30ec\u30d3\u306e\u8996\u8074\u3001\u305d\u306e\u65e5\u306e\u5929\u6c17\u306a\u3069\u30e9\u30a4...&quot; http://tumblr.com/xjy1cwiwrm","id":30604423327653888,"from_user_id":6113557,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.tumblr.com/&quot; rel=&quot;nofollow&quot;&gt;Tumblr&lt;/a&gt;"},{"from_user_id_str":"118092912","profile_image_url":"http://a3.twimg.com/profile_images/1225330653/____normal.PNG","created_at":"Thu, 27 Jan 2011 12:29:13 +0000","from_user":"saechuchun","id_str":"30603231042207744","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u3067\u3082\u5730\u9707\u306e\u8a71\u984c\u306e\u3068\u304d\u306e\u300c\u3082\u3057\u6771\u4eac\u3067\u76f4\u4e0b\u578b\u5730\u9707\u304c\u8d77\u304d\u305f\u3089\u300d\u3068\u304b\u5929\u6c17\u4e88\u5831\u306e\u3068\u304d\u306f\u897f\u3067\u96e8\u3060\u308d\u3046\u3068\u300c\u6771\u4eac\u3067\u306f\u301c\u65e5\u9593\u4e7e\u71e5\u3057\u3066\u307e\u3059\u306d\u30fc\u300d\u3068\u304b\u305d\u3046\u3044\u3046\u30ad\u30fc\u5c40\u306e\u95a2\u6771\u4e2d\u5fc3\u4e3b\u7fa9\u306f\u597d\u304d\u3067\u306f\u306a\u3044","id":30603231042207744,"from_user_id":118092912,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twtr.jp&quot; rel=&quot;nofollow&quot;&gt;Keitai Web&lt;/a&gt;"}],"max_id":30724239224995840,"since_id":28179956995457024,"refresh_url":"?since_id=30724239224995840&q=%E6%9D%B1%E4%BA%AC%E3%80%80%E5%A4%A9%E6%B0%97","next_page":"?page=2&max_id=30724239224995840&rpp=50&lang=ja&q=%E6%9D%B1%E4%BA%AC%E3%80%80%E5%A4%A9%E6%B0%97","results_per_page":50,"page":1,"completed_in":0.170202,"warning":"adjusted since_id to 28179956995457024 (), requested since_id was older than allowed -- since_id removed for pagination.","since_id_str":"28179956995457024","max_id_str":"30724239224995840","query":"%E6%9D%B1%E4%BA%AC%E3%80%80%E5%A4%A9%E6%B0%97"}
+ test/json-data/numbers.json view
@@ -0,0 +1,1 @@+[1.15, 1.3224999999999998, 1.5208749999999998, 1.7490062499999994, 2.0113571874999994, 2.313060765624999, 2.6600198804687487, 3.0590228625390607, 3.5178762919199196, 4.045557735707907, 4.652391396064092, 5.350250105473706, 6.152787621294761, 7.075705764488975, 8.137061629162321, 9.357620873536668, 10.761264004567169, 12.375453605252241, 14.231771646040077, 16.36653739294609, 18.821518001888, 21.644745702171196, 24.891457557496874, 28.625176191121405, 32.918952619789614, 37.856795512758055, 43.535314839671756, 50.06561206562252, 57.57545387546589, 66.21177195678577, 76.14353775030362, 87.56506841284916, 100.69982867477653, 115.804802975993, 133.17552342239193, 153.15185193575073, 176.1246297261133, 202.5433241850303, 232.9248228127848, 267.86354623470254, 308.04307816990786, 354.24953989539404, 407.3869708797031, 468.49501651165855, 538.7692689884072, 619.5846593366683, 712.5223582371685, 819.4007119727437, 942.3108187686552, 1083.6574415839534, 1246.2060578215462, 1433.136966494778, 1648.1075114689947, 1895.323638189344, 2179.622183917745, 2506.565511505407, 2882.5503382312177, 3314.9328889659, 3812.172822310785, 4383.998745657402, 5041.598557506012, 5797.838341131914, 6667.5140923017, 7667.641206146955, 8817.787387068996, 10140.455495129345, 11661.523819398746, 13410.752392308557, 15422.365251154839, 17735.720038828065, 20396.078044652273, 23455.489751350113, 26973.813214052625, 31019.885196160518, 35672.867975584595, 41023.79817192228, 47177.36789771062, 54253.97308236721, 62392.06904472228, 71750.87940143062, 82513.5113116452, 94890.53800839197, 109124.11870965076, 125492.73651609836, 144316.6469935131, 165964.14404254008, 190858.76564892105, 219487.5804962592, 252410.71757069806, 290272.32520630275, 333813.17398724816, 383885.1500853353, 441467.9225981356, 507688.1109878559, 583841.3276360342, 671417.5267814393, 772130.1557986551, 887949.6791684533, 1021142.1310437213, 1174313.4507002793, 1350460.4683053212, 1553029.5385511192, 1785983.969333787, 2053881.564733855, 2361963.7994439327, 2716258.369360523, 3123697.124764601, 3592251.6934792907, 4131089.447501184, 4750752.864626361, 5463365.794320315, 6282870.663468362, 7225301.262988616, 8309096.452436907, 9555460.920302443, 10988780.058347808, 12637097.067099977, 14532661.627164973, 16712560.871239718, 19219445.001925673, 22102361.752214525, 25417716.0150467, 29230373.417303704, 33614929.42989926, 38657168.84438414, 44455744.17104176, 51124105.79669802, 58792721.66620272, 67611629.91613312, 77753374.40355308, 89416380.56408603, 102828837.64869894, 118253163.29600377, 135991137.79040432, 156389808.45896497, 179848279.7278097, 206825521.68698114, 237849349.94002828, 273526752.4310325, 314555765.2956874, 361739130.09004045, 415999999.60354644, 478399999.5440784, 550159999.4756901, 632683999.3970436, 727586599.3066001, 836724589.20259, 962233277.5829784, 1106568269.2204251, 1272553509.6034887, 1463436536.044012, 1682952016.4506137, 1935394818.9182055, 2225704041.755936, 2559559648.019326, 2943493595.222225, 3385017634.5055585, 3892770279.681392, 4476685821.6336, 5148188694.87864, 5920416999.1104355, 6808479548.977001, 7829751481.32355, 9004214203.522081, 10354846334.050394, 11908073284.157951, 13694284276.781643, 15748426918.29889, 18110690956.04372, 20827294599.450275, 23951388789.367817, 27544097107.772987, 31675711673.938934, 36427068425.02977, 41891128688.78423, 48174797992.10186, 55401017690.91714, 63711170344.5547, 73267845896.2379, 84258022780.67358, 96896726197.77461, 111431235127.4408, 128145920396.5569, 147367808456.04044, 169472979724.44647, 194893926683.11343, 224128015685.58044, 257747218038.41748, 296409300744.1801, 340870695855.80707, 392001300234.1781, 450801495269.3048, 518421719559.70044, 596184977493.6555, 685612724117.7037, 788454632735.3593, 906722827645.6631, 1042731251792.5125, 1199140939561.3892, 1379012080495.5974, 1585863892569.937, 1823743476455.4275, 2097304997923.7415, 2411900747612.3022, 2773685859754.1475, 3189738738717.2695, 3668199549524.8594, 4218429481953.5884, 4851193904246.626, 5578872989883.619, 6415703938366.162, 7378059529121.086, 8484768458489.248, 9757483727262.635, 11221106286352.03, 12904272229304.832, 14839913063700.555, 17065900023255.637, 19625785026743.98, 22569652780755.58, 25955100697868.91, 29848365802549.246, 34325620672931.63, 39474463773871.375, 45395633339952.07, 52204978340944.88, 60035725092086.61, 69041083855899.59, 79397246434284.53, 91306833399427.2, 105002858409341.27, 120753287170742.45, 138866280246353.81, 159696222283306.88, 183650655625802.88, 211198253969673.3, 242877992065124.28, 279309690874892.9, 321206144506126.8, 369387066182045.8, 424795126109352.6, 488514395025755.5, 561791554279618.75, 646060287421561.5, 742969330534795.8, 854414730115015.0, 982576939632267.1, 1129963480577107.2, 1299458002663673.2, 1494376703063224.0, 1718533208522707.5, 1976313189801113.5, 2272760168271280.5, 2613674193511972.0, 3005725322538767.5, 3456584120919582.5, 3975071739057519.5, 4571332499916147.0, 5257032374903569.0, 6045587231139104.0, 6952425315809969.0, 7995289113181464.0, 9194582480158682.0, 1.0573769852182484e+16, 1.2159835330009856e+16, 1.3983810629511332e+16, 1.6081382223938032e+16, 1.8493589557528736e+16, 2.1267627991158044e+16, 2.4457772189831748e+16, 2.8126438018306508e+16, 3.234540372105248e+16, 3.719721427921035e+16, 4.27767964210919e+16, 4.919331588425568e+16, 5.657231326689403e+16, 6.505816025692813e+16, 7.481688429546734e+16, 8.603941693978744e+16, 9.894532948075555e+16, 1.1378712890286886e+17, 1.3085519823829918e+17, 1.5048347797404406e+17, 1.7305599967015066e+17, 1.9901439962067325e+17, 2.288665595637742e+17, 2.6319654349834032e+17, 3.026760250230913e+17, 3.48077428776555e+17, 4.002890430930382e+17, 4.603323995569939e+17, 5.29382259490543e+17, 6.087895984141244e+17, 7.00108038176243e+17, 8.051242439026793e+17, 9.258928804880812e+17, 1.0647768125612933e+18, 1.224493334445487e+18, 1.4081673346123103e+18, 1.6193924348041567e+18, 1.8623013000247798e+18, 2.1416464950284966e+18, 2.462893469282771e+18, 2.8323274896751867e+18, 3.257176613126464e+18, 3.7457531050954337e+18, 4.3076160708597484e+18, 4.95375848148871e+18, 5.696822253712016e+18, 6.551345591768818e+18, 7.53404743053414e+18, 8.66415454511426e+18, 9.963777726881399e+18, 1.1458344385913608e+19, 1.3177096043800648e+19, 1.5153660450370744e+19, 1.7426709517926355e+19, 2.0040715945615307e+19, 2.30468233374576e+19, 2.650384683807624e+19, 3.047942386378767e+19, 3.505133744335582e+19, 4.030903805985919e+19, 4.635539376883806e+19, 5.330870283416377e+19, 6.130500825928833e+19, 7.0500759498181575e+19, 8.10758734229088e+19, 9.323725443634512e+19, 1.0722284260179688e+20, 1.233062689920664e+20, 1.4180220934087634e+20, 1.6307254074200778e+20, 1.8753342185330894e+20, 2.1566343513130526e+20, 2.4801295040100103e+20, 2.8521489296115116e+20, 3.2799712690532385e+20, 3.7719669594112236e+20, 4.337762003322907e+20, 4.988426303821342e+20, 5.7366902493945437e+20, 6.597193786803724e+20, 7.586772854824282e+20, 8.724788783047925e+20, 1.0033507100505113e+21, 1.1538533165580878e+21, 1.326931314041801e+21, 1.5259710111480708e+21, 1.7548666628202813e+21, 2.0180966622433234e+21, 2.3208111615798217e+21, 2.668932835816795e+21, 3.0692727611893135e+21, 3.529663675367711e+21, 4.059113226672867e+21, 4.667980210673796e+21, 5.368177242274866e+21, 6.173403828616095e+21, 7.099414402908508e+21, 8.164326563344784e+21, 9.388975547846502e+21, 1.0797321880023475e+22, 1.2416920162026996e+22, 1.4279458186331043e+22, 1.64213769142807e+22, 1.8884583451422802e+22, 2.171727096913622e+22, 2.497486161450665e+22, 2.872109085668265e+22, 3.3029254485185042e+22, 3.798364265796279e+22, 4.368118905665721e+22, 5.0233367415155795e+22, 5.7768372527429155e+22, 6.643362840654352e+22, 7.639867266752504e+22, 8.78584735676538e+22, 1.0103724460280185e+23, 1.1619283129322213e+23, 1.3362175598720545e+23, 1.5366501938528623e+23, 1.7671477229307915e+23, 2.0322198813704103e+23, 2.3370528635759717e+23, 2.687610793112367e+23, 3.090752412079222e+23, 3.554365273891105e+23, 4.0875200649747705e+23, 4.700648074720986e+23, 5.405745285929133e+23, 6.216607078818502e+23, 7.149098140641278e+23, 8.221462861737468e+23, 9.454682290998088e+23, 1.0872884634647801e+24, 1.250381732984497e+24, 1.4379389929321714e+24, 1.653629841871997e+24, 1.9016743181527962e+24, 2.1869254658757156e+24, 2.5149642857570727e+24, 2.8922089286206334e+24, 3.326040267913728e+24, 3.824946308100787e+24, 4.3986882543159046e+24, 5.05849149246329e+24, 5.817265216332783e+24, 6.6898549987827e+24, 7.693333248600105e+24, 8.84733323589012e+24, 1.0174433221273637e+25, 1.1700598204464681e+25, 1.3455687935134384e+25, 1.547404112540454e+25, 1.779514729421522e+25, 2.04644193883475e+25, 2.353408229659962e+25, 2.7064194641089563e+25, 3.1123823837253e+25, 3.5792397412840944e+25, 4.116125702476708e+25, 4.733544557848214e+25, 5.443576241525446e+25, 6.260112677754262e+25, 7.199129579417401e+25, 8.27899901633001e+25, 9.52084886877951e+25, 1.0948976199096437e+26, 1.25913226289609e+26, 1.4480021023305035e+26, 1.665202417680079e+26, 1.9149827803320907e+26, 2.202230197381904e+26, 2.5325647269891895e+26, 2.9124494360375676e+26, 3.349316851443203e+26, 3.8517143791596826e+26, 4.429471536033635e+26, 5.09389226643868e+26, 5.857976106404481e+26, 6.736672522365153e+26, 7.747173400719924e+26, 8.909249410827912e+26, 1.0245636822452099e+27, 1.1782482345819913e+27, 1.35498546976929e+27, 1.5582332902346833e+27, 1.7919682837698857e+27, 2.0607635263353683e+27, 2.3698780552856732e+27, 2.725359763578524e+27, 3.1341637281153025e+27, 3.6042882873325974e+27, 4.1449315304324867e+27, 4.7666712599973594e+27, 5.481671948996963e+27, 6.303922741346507e+27, 7.249511152548482e+27, 8.336937825430755e+27, 9.587478499245366e+27, 1.102560027413217e+28, 1.2679440315251995e+28, 1.4581356362539794e+28, 1.6768559816920761e+28, 1.9283843789458875e+28, 2.2176420357877703e+28, 2.5502883411559357e+28, 2.932831592329326e+28, 3.3727563311787245e+28, 3.8786697808555327e+28, 4.460470247983863e+28, 5.129540785181441e+28, 5.8989719029586575e+28, 6.783817688402455e+28, 7.801390341662823e+28, 8.971598892912247e+28, 1.0317338726849081e+29, 1.1864939535876443e+29, 1.3644680466257909e+29, 1.5691382536196593e+29, 1.8045089916626083e+29, 2.0751853404119993e+29, 2.3864631414737988e+29, 2.7444326126948684e+29, 3.1560975045990984e+29, 3.629512130288963e+29, 4.173938949832307e+29, 4.800029792307153e+29, 5.520034261153225e+29, 6.348039400326208e+29, 7.300245310375139e+29, 8.39528210693141e+29, 9.65457442297112e+29, 1.1102760586416787e+30, 1.2768174674379305e+30, 1.46834008755362e+30, 1.6885911006866628e+30, 1.941879765789662e+30, 2.2331617306581113e+30, 2.568135990256828e+30, 2.9533563887953516e+30, 3.396359847114654e+30, 3.905813824181852e+30, 4.491685897809129e+30, 5.165438782480498e+30, 5.940254599852573e+30, 6.831292789830458e+30, 7.855986708305026e+30, 9.034384714550779e+30, 1.0389542421733396e+31, 1.1947973784993405e+31, 1.3740169852742412e+31, 1.5801195330653773e+31, 1.817137463025184e+31, 2.0897080824789613e+31, 2.4031642948508052e+31, 2.7636389390784257e+31, 3.1781847799401893e+31, 3.6549124969312175e+31, 4.2031493714709e+31, 4.833621777191535e+31, 5.558665043770264e+31, 6.392464800335803e+31, 7.3513345203861735e+31, 8.454034698444098e+31, 9.722139903210713e+31, 1.1180460888692319e+32, 1.2857530021996165e+32, 1.478615952529559e+32, 1.7004083454089925e+32, 1.9554695972203414e+32, 2.2487900368033926e+32, 2.586108542323901e+32, 2.974024823672486e+32, 3.420128547223359e+32, 3.933147829306862e+32, 4.523120003702891e+32, 5.2015880042583246e+32, 5.981826204897073e+32, 6.879100135631634e+32, 7.910965155976377e+32, 9.097609929372833e+32, 1.0462251418778757e+33, 1.2031589131595571e+33, 1.3836327501334904e+33, 1.591177662653514e+33, 1.8298543120515409e+33, 2.104332458859272e+33, 2.4199823276881623e+33, 2.7829796768413863e+33, 3.200426628367594e+33, 3.6804906226227334e+33, 4.232564216016143e+33, 4.867448848418564e+33, 5.597566175681348e+33, 6.437201102033549e+33, 7.402781267338582e+33, 8.513198457439368e+33, 9.790178226055273e+33, 1.1258704959963564e+34, 1.2947510703958096e+34, 1.488963730955181e+34, 1.712308290598458e+34, 1.9691545341882266e+34, 2.2645277143164603e+34, 2.604206871463929e+34, 2.994837902183518e+34, 3.4440635875110457e+34, 3.960673125637702e+34, 4.554774094483357e+34, 5.237990208655861e+34, 6.023688739954239e+34, 6.927242050947375e+34, 7.96632835858948e+34, 9.161277612377902e+34, 1.0535469254234585e+35, 1.2115789642369772e+35, 1.3933158088725236e+35, 1.6023131802034022e+35, 1.8426601572339122e+35, 2.119059180818999e+35, 2.4369180579418488e+35, 2.8024557666331258e+35, 3.2228241316280944e+35, 3.706247751372308e+35, 4.262184914078154e+35, 4.901512651189877e+35, 5.636739548868358e+35, 6.482250481198611e+35, 7.454588053378403e+35, 8.572776261385162e+35, 9.858692700592935e+35, 1.1337496605681875e+36, 1.3038121096534156e+36, 1.499383926101428e+36, 1.7242915150166418e+36, 1.982935242269138e+36, 2.2803755286095084e+36]
+ test/json-data/twitter1.json view
@@ -0,0 +1,1 @@+{"results":[{"from_user_id_str":"80430860","profile_image_url":"http://a2.twimg.com/profile_images/536455139/icon32_normal.png","created_at":"Wed, 26 Jan 2011 07:07:02 +0000","from_user":"kazu_yamamoto","id_str":"30159761706061824","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Haskell Server Pages \u3063\u3066\u3001\u307e\u3060\u7d9a\u3044\u3066\u3044\u305f\u306e\u304b\uff01","id":30159761706061824,"from_user_id":80430860,"geo":null,"iso_language_code":"no","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"}],"max_id":30159761706061824,"since_id":0,"refresh_url":"?since_id=30159761706061824&q=haskell","next_page":"?page=2&max_id=30159761706061824&rpp=1&q=haskell","results_per_page":1,"page":1,"completed_in":0.012606,"since_id_str":"0","max_id_str":"30159761706061824","query":"haskell"}
+ test/json-data/twitter10.json view
@@ -0,0 +1,1 @@+{"results":[{"from_user_id_str":"207858021","profile_image_url":"http://a3.twimg.com/sticky/default_profile_images/default_profile_2_normal.png","created_at":"Wed, 26 Jan 2011 04:30:38 +0000","from_user":"pboudarga","id_str":"30120402839666689","metadata":{"result_type":"recent"},"to_user_id":null,"text":"I'm at Rolla Sushi Grill (27737 Bouquet Canyon Road, #106, Btw Haskell Canyon and Rosedell Drive, Saugus) http://4sq.com/gqqdhs","id":30120402839666689,"from_user_id":207858021,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://foursquare.com&quot; rel=&quot;nofollow&quot;&gt;foursquare&lt;/a&gt;"},{"from_user_id_str":"69988683","profile_image_url":"http://a0.twimg.com/profile_images/1211955817/avatar_7888_normal.gif","created_at":"Wed, 26 Jan 2011 04:25:23 +0000","from_user":"YNK33","id_str":"30119083059978240","metadata":{"result_type":"recent"},"to_user_id":null,"text":"hsndfile 0.5.0: Free and open source Haskell bindings for libsndfile http://bit.ly/gHaBWG Mac Os","id":30119083059978240,"from_user_id":69988683,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"81492","profile_image_url":"http://a1.twimg.com/profile_images/423894208/Picture_7_normal.jpg","created_at":"Wed, 26 Jan 2011 04:24:28 +0000","from_user":"satzz","id_str":"30118851488251904","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Emacs\u306e\u30e2\u30fc\u30c9\u8868\u793a\u304c\u4eca(Ruby Controller Outputz RoR Flymake REl hs)\u3068\u306a\u3063\u3066\u3066\u3088\u304f\u308f\u304b\u3089\u306a\u3044\u3093\u3060\u3051\u3069\u6700\u5f8c\u306eREl\u3068\u304bhs\u3063\u3066\u4f55\u3060\u308d\u3046\u2026haskell\u3068\u304b2\u5e74\u4ee5\u4e0a\u66f8\u3044\u3066\u306a\u3044\u3051\u3069\u2026","id":30118851488251904,"from_user_id":81492,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.hootsuite.com&quot; rel=&quot;nofollow&quot;&gt;HootSuite&lt;/a&gt;"},{"from_user_id_str":"9518356","profile_image_url":"http://a2.twimg.com/profile_images/119165723/ocaml-icon_normal.png","created_at":"Wed, 26 Jan 2011 04:19:19 +0000","from_user":"planet_ocaml","id_str":"30117557788741632","metadata":{"result_type":"recent"},"to_user_id":null,"text":"I so miss #haskell type classes in #ocaml - i want to do something like refinement. Also why does ocaml not have... http://bit.ly/geYRwt","id":30117557788741632,"from_user_id":9518356,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"218059","profile_image_url":"http://a1.twimg.com/profile_images/1053837723/twitter-icon9_normal.jpg","created_at":"Wed, 26 Jan 2011 04:16:32 +0000","from_user":"aprikip","id_str":"30116854940835840","metadata":{"result_type":"recent"},"to_user_id":null,"text":"yatex-mode\u3084haskell-mode\u306e\u3053\u3068\u3067\u3059\u306d\u3001\u308f\u304b\u308a\u307e\u3059\u3002","id":30116854940835840,"from_user_id":218059,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://sites.google.com/site/yorufukurou/&quot; rel=&quot;nofollow&quot;&gt;YoruFukurou&lt;/a&gt;"},{"from_user_id_str":"216363","profile_image_url":"http://a1.twimg.com/profile_images/72454310/Tim-Avatar_normal.png","created_at":"Wed, 26 Jan 2011 04:15:30 +0000","from_user":"dysinger","id_str":"30116594684264448","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Haskell in Hawaii tonight for me... #fun","id":30116594684264448,"from_user_id":216363,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.nambu.com/&quot; rel=&quot;nofollow&quot;&gt;Nambu&lt;/a&gt;"},{"from_user_id_str":"1774820","profile_image_url":"http://a2.twimg.com/profile_images/61169291/dan_desert_thumb_normal.jpg","created_at":"Wed, 26 Jan 2011 04:13:36 +0000","from_user":"DanMil","id_str":"30116117682851840","metadata":{"result_type":"recent"},"to_user_id":1594784,"text":"@ojrac @chewedwire @tomheon Haskell isn't a language, it's a belief system.  A seductive one...","id":30116117682851840,"from_user_id":1774820,"to_user":"ojrac","geo":null,"iso_language_code":"en","to_user_id_str":"1594784","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"659256","profile_image_url":"http://a0.twimg.com/profile_images/746976711/angular-final_normal.jpg","created_at":"Wed, 26 Jan 2011 04:11:06 +0000","from_user":"djspiewak","id_str":"30115488931520512","metadata":{"result_type":"recent"},"to_user_id":null,"text":"One of the very nice things about Haskell as opposed to SML is the reduced proliferation of identifiers (e.g. andb, orb, etc). #typeclasses","id":30115488931520512,"from_user_id":659256,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://itunes.apple.com/us/app/twitter/id409789998?mt=12&quot; rel=&quot;nofollow&quot;&gt;Twitter for Mac&lt;/a&gt;"},{"from_user_id_str":"144546280","profile_image_url":"http://a1.twimg.com/a/1295051201/images/default_profile_1_normal.png","created_at":"Wed, 26 Jan 2011 04:06:12 +0000","from_user":"listwarenet","id_str":"30114255890026496","metadata":{"result_type":"recent"},"to_user_id":null,"text":"http://www.listware.net/201101/haskell-cafe/84752-re-haskell-cafe-gpl-license-of-h-matrix-and-prelude-numeric.html Re: Haskell-c","id":30114255890026496,"from_user_id":144546280,"geo":null,"iso_language_code":"no","to_user_id_str":null,"source":"&lt;a href=&quot;http://1e10.org/cloud/&quot; rel=&quot;nofollow&quot;&gt;1e10&lt;/a&gt;"},{"from_user_id_str":"1594784","profile_image_url":"http://a2.twimg.com/profile_images/378515773/square-profile_normal.jpg","created_at":"Wed, 26 Jan 2011 04:01:29 +0000","from_user":"ojrac","id_str":"30113067333324800","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @tomheon: @ojrac @chewedwire Don't worry, learning Haskell will not give you any clear idea what monad means.","id":30113067333324800,"from_user_id":1594784,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"}],"max_id":30120402839666689,"since_id":0,"refresh_url":"?since_id=30120402839666689&q=haskell","next_page":"?page=2&max_id=30120402839666689&rpp=10&q=haskell","results_per_page":10,"page":1,"completed_in":0.012714,"since_id_str":"0","max_id_str":"30120402839666689","query":"haskell"}
+ test/json-data/twitter100.json view
@@ -0,0 +1,1 @@+{"results":[{"from_user_id_str":"3646730","profile_image_url":"http://a3.twimg.com/profile_images/404973767/avatar_normal.jpg","created_at":"Wed, 26 Jan 2011 04:35:07 +0000","from_user":"nicolaslara","id_str":"30121530767708160","metadata":{"result_type":"recent"},"to_user_id":18616016,"text":"@josej30 Python y Clojure. Obviamente son diferentes, y cada uno tiene sus ventajas y desventajas. De Haskell faltar\u00eda pattern matching","id":30121530767708160,"from_user_id":3646730,"to_user":"josej30","geo":null,"iso_language_code":"es","to_user_id_str":"18616016","source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"207858021","profile_image_url":"http://a3.twimg.com/sticky/default_profile_images/default_profile_2_normal.png","created_at":"Wed, 26 Jan 2011 04:30:38 +0000","from_user":"pboudarga","id_str":"30120402839666689","metadata":{"result_type":"recent"},"to_user_id":null,"text":"I'm at Rolla Sushi Grill (27737 Bouquet Canyon Road, #106, Btw Haskell Canyon and Rosedell Drive, Saugus) http://4sq.com/gqqdhs","id":30120402839666689,"from_user_id":207858021,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://foursquare.com&quot; rel=&quot;nofollow&quot;&gt;foursquare&lt;/a&gt;"},{"from_user_id_str":"69988683","profile_image_url":"http://a0.twimg.com/profile_images/1211955817/avatar_7888_normal.gif","created_at":"Wed, 26 Jan 2011 04:25:23 +0000","from_user":"YNK33","id_str":"30119083059978240","metadata":{"result_type":"recent"},"to_user_id":null,"text":"hsndfile 0.5.0: Free and open source Haskell bindings for libsndfile http://bit.ly/gHaBWG Mac Os","id":30119083059978240,"from_user_id":69988683,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"81492","profile_image_url":"http://a1.twimg.com/profile_images/423894208/Picture_7_normal.jpg","created_at":"Wed, 26 Jan 2011 04:24:28 +0000","from_user":"satzz","id_str":"30118851488251904","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Emacs\u306e\u30e2\u30fc\u30c9\u8868\u793a\u304c\u4eca(Ruby Controller Outputz RoR Flymake REl hs)\u3068\u306a\u3063\u3066\u3066\u3088\u304f\u308f\u304b\u3089\u306a\u3044\u3093\u3060\u3051\u3069\u6700\u5f8c\u306eREl\u3068\u304bhs\u3063\u3066\u4f55\u3060\u308d\u3046\u2026haskell\u3068\u304b2\u5e74\u4ee5\u4e0a\u66f8\u3044\u3066\u306a\u3044\u3051\u3069\u2026","id":30118851488251904,"from_user_id":81492,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.hootsuite.com&quot; rel=&quot;nofollow&quot;&gt;HootSuite&lt;/a&gt;"},{"from_user_id_str":"9518356","profile_image_url":"http://a2.twimg.com/profile_images/119165723/ocaml-icon_normal.png","created_at":"Wed, 26 Jan 2011 04:19:19 +0000","from_user":"planet_ocaml","id_str":"30117557788741632","metadata":{"result_type":"recent"},"to_user_id":null,"text":"I so miss #haskell type classes in #ocaml - i want to do something like refinement. Also why does ocaml not have... http://bit.ly/geYRwt","id":30117557788741632,"from_user_id":9518356,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"218059","profile_image_url":"http://a1.twimg.com/profile_images/1053837723/twitter-icon9_normal.jpg","created_at":"Wed, 26 Jan 2011 04:16:32 +0000","from_user":"aprikip","id_str":"30116854940835840","metadata":{"result_type":"recent"},"to_user_id":null,"text":"yatex-mode\u3084haskell-mode\u306e\u3053\u3068\u3067\u3059\u306d\u3001\u308f\u304b\u308a\u307e\u3059\u3002","id":30116854940835840,"from_user_id":218059,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://sites.google.com/site/yorufukurou/&quot; rel=&quot;nofollow&quot;&gt;YoruFukurou&lt;/a&gt;"},{"from_user_id_str":"216363","profile_image_url":"http://a1.twimg.com/profile_images/72454310/Tim-Avatar_normal.png","created_at":"Wed, 26 Jan 2011 04:15:30 +0000","from_user":"dysinger","id_str":"30116594684264448","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Haskell in Hawaii tonight for me... #fun","id":30116594684264448,"from_user_id":216363,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.nambu.com/&quot; rel=&quot;nofollow&quot;&gt;Nambu&lt;/a&gt;"},{"from_user_id_str":"1774820","profile_image_url":"http://a2.twimg.com/profile_images/61169291/dan_desert_thumb_normal.jpg","created_at":"Wed, 26 Jan 2011 04:13:36 +0000","from_user":"DanMil","id_str":"30116117682851840","metadata":{"result_type":"recent"},"to_user_id":1594784,"text":"@ojrac @chewedwire @tomheon Haskell isn't a language, it's a belief system.  A seductive one...","id":30116117682851840,"from_user_id":1774820,"to_user":"ojrac","geo":null,"iso_language_code":"en","to_user_id_str":"1594784","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"659256","profile_image_url":"http://a0.twimg.com/profile_images/746976711/angular-final_normal.jpg","created_at":"Wed, 26 Jan 2011 04:11:06 +0000","from_user":"djspiewak","id_str":"30115488931520512","metadata":{"result_type":"recent"},"to_user_id":null,"text":"One of the very nice things about Haskell as opposed to SML is the reduced proliferation of identifiers (e.g. andb, orb, etc). #typeclasses","id":30115488931520512,"from_user_id":659256,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://itunes.apple.com/us/app/twitter/id409789998?mt=12&quot; rel=&quot;nofollow&quot;&gt;Twitter for Mac&lt;/a&gt;"},{"from_user_id_str":"144546280","profile_image_url":"http://a1.twimg.com/a/1295051201/images/default_profile_1_normal.png","created_at":"Wed, 26 Jan 2011 04:06:12 +0000","from_user":"listwarenet","id_str":"30114255890026496","metadata":{"result_type":"recent"},"to_user_id":null,"text":"http://www.listware.net/201101/haskell-cafe/84752-re-haskell-cafe-gpl-license-of-h-matrix-and-prelude-numeric.html Re: Haskell-c","id":30114255890026496,"from_user_id":144546280,"geo":null,"iso_language_code":"no","to_user_id_str":null,"source":"&lt;a href=&quot;http://1e10.org/cloud/&quot; rel=&quot;nofollow&quot;&gt;1e10&lt;/a&gt;"},{"from_user_id_str":"1594784","profile_image_url":"http://a2.twimg.com/profile_images/378515773/square-profile_normal.jpg","created_at":"Wed, 26 Jan 2011 04:01:29 +0000","from_user":"ojrac","id_str":"30113067333324800","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @tomheon: @ojrac @chewedwire Don't worry, learning Haskell will not give you any clear idea what monad means.","id":30113067333324800,"from_user_id":1594784,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"207589736","profile_image_url":"http://a3.twimg.com/profile_images/1225527428/headshot_1_normal.jpg","created_at":"Wed, 26 Jan 2011 04:00:13 +0000","from_user":"ashleevelazq101","id_str":"30112747555397632","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Federal investigation finds safety violations at The Acadia Hospital: By Meg Haskell, BDN Staff The investigatio... http://bit.ly/dONnpn","id":30112747555397632,"from_user_id":207589736,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"17671137","profile_image_url":"http://a2.twimg.com/profile_images/290264834/Haskell-logo-outer-glow_normal.png","created_at":"Wed, 26 Jan 2011 03:58:00 +0000","from_user":"Hackage","id_str":"30112192346984448","metadata":{"result_type":"recent"},"to_user_id":null,"text":"streams 0.4, added by EdwardKmett: Various Haskell 2010 stream comonads http://bit.ly/idBkPe","id":30112192346984448,"from_user_id":17671137,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"17489366","profile_image_url":"http://a3.twimg.com/sticky/default_profile_images/default_profile_2_normal.png","created_at":"Wed, 26 Jan 2011 03:58:00 +0000","from_user":"aapnoot","id_str":"30112191881420800","metadata":{"result_type":"recent"},"to_user_id":null,"text":"streams 0.4, added by EdwardKmett: Various Haskell 2010 stream comonads http://bit.ly/idBkPe","id":30112191881420800,"from_user_id":17489366,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"8530482","profile_image_url":"http://a2.twimg.com/profile_images/137867266/n608671563_7396_normal.jpg","created_at":"Wed, 26 Jan 2011 03:50:12 +0000","from_user":"jeffmclamb","id_str":"30110229207187456","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Angel - daemon to run and monitor processes like daemontools or god, written in Haskell http://ff.im/-wNyLk","id":30110229207187456,"from_user_id":8530482,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://friendfeed.com&quot; rel=&quot;nofollow&quot;&gt;FriendFeed&lt;/a&gt;"},{"from_user_id_str":"177539201","profile_image_url":"http://a0.twimg.com/profile_images/1178368800/img_normal.jpeg","created_at":"Wed, 26 Jan 2011 03:46:01 +0000","from_user":"tomheon","id_str":"30109174645919744","metadata":{"result_type":"recent"},"to_user_id":1594784,"text":"@ojrac @chewedwire Don't worry, learning Haskell will not give you any clear idea what monad means.","id":30109174645919744,"from_user_id":177539201,"to_user":"ojrac","geo":null,"iso_language_code":"en","to_user_id_str":"1594784","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"1594784","profile_image_url":"http://a2.twimg.com/profile_images/378515773/square-profile_normal.jpg","created_at":"Wed, 26 Jan 2011 03:44:34 +0000","from_user":"ojrac","id_str":"30108808684503040","metadata":{"result_type":"recent"},"to_user_id":128028225,"text":"@chewedwire @tomheon Why are you making me curious about Haskell? I LIKE not knowing what monad means!!","id":30108808684503040,"from_user_id":1594784,"to_user":"chewedwire","geo":null,"iso_language_code":"en","to_user_id_str":"128028225","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"23750094","profile_image_url":"http://a0.twimg.com/profile_images/951373780/Moeinthecar_normal.jpg","created_at":"Wed, 26 Jan 2011 03:41:54 +0000","from_user":"shokalshab","id_str":"30108140443795456","metadata":{"result_type":"recent"},"to_user_id":null,"text":"I'm at Magnitude Cheer @ Gymnastics Olympica USA (7735 Haskell Ave., btw Saticoy &amp; Strathern, Van Nuys) http://4sq.com/gmXfaL","id":30108140443795456,"from_user_id":23750094,"geo":null,"iso_language_code":"en","place":{"id":"4e4a2a2f86cb2946","type_":"poi","full_name":"Gymnastics Olympica USA, Van Nuys"},"to_user_id_str":null,"source":"&lt;a href=&quot;http://foursquare.com&quot; rel=&quot;nofollow&quot;&gt;foursquare&lt;/a&gt;"},{"from_user_id_str":"8135112","profile_image_url":"http://a0.twimg.com/profile_images/1165240350/LIMITED_normal.jpg","created_at":"Wed, 26 Jan 2011 03:41:35 +0000","from_user":"Claricei","id_str":"30108059208515584","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @kristenmchugh22: @KeithOlbermann Ryan looks like Jughead w/o the hat. And sounds as mealy-mouthed as Eddie Haskell.","id":30108059208515584,"from_user_id":8135112,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"128028225","profile_image_url":"http://a1.twimg.com/profile_images/1224994593/Coding_Drunk_normal.jpg","created_at":"Wed, 26 Jan 2011 03:40:02 +0000","from_user":"chewedwire","id_str":"30107670367182848","metadata":{"result_type":"recent"},"to_user_id":177539201,"text":"@tomheon Cool, I'll take a look. I feel like I should mention this: http://bit.ly/hVstDM","id":30107670367182848,"from_user_id":128028225,"to_user":"tomheon","geo":null,"iso_language_code":"en","to_user_id_str":"177539201","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"8679778","profile_image_url":"http://a3.twimg.com/profile_images/1195318056/me_nyc_12_18_10_icon_normal.jpg","created_at":"Wed, 26 Jan 2011 03:38:42 +0000","from_user":"kristenmchugh22","id_str":"30107332381777920","metadata":{"result_type":"recent"},"to_user_id":756269,"text":"@KeithOlbermann Ryan looks like Jughead w/o the hat. And sounds as mealy-mouthed as Eddie Haskell.","id":30107332381777920,"from_user_id":8679778,"to_user":"KeithOlbermann","geo":null,"iso_language_code":"en","to_user_id_str":"756269","source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"103316559","profile_image_url":"http://a1.twimg.com/profile_images/1179458751/bc3beab4-d59d-4e78-b13b-50747986cfa2_normal.png","created_at":"Wed, 26 Jan 2011 03:36:15 +0000","from_user":"cityslikr","id_str":"30106719153557504","metadata":{"result_type":"recent"},"to_user_id":null,"text":"&quot;Social safety net into a hammock.&quot; So says Eddie Haskell with the GOP response. #SOTU","id":30106719153557504,"from_user_id":103316559,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://blackberry.com/twitter&quot; rel=&quot;nofollow&quot;&gt;Twitter for BlackBerry\u00ae&lt;/a&gt;"},{"from_user_id_str":"169063143","profile_image_url":"http://a0.twimg.com/profile_images/1160506212/9697_1_normal.gif","created_at":"Wed, 26 Jan 2011 03:31:52 +0000","from_user":"wrkforce_safety","id_str":"30105614919143424","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Federal investigation finds safety violations at The Acadia Hospital: By Meg Haskell, BDN Staff The investigatio... http://bit.ly/gA60C1","id":30105614919143424,"from_user_id":169063143,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"177539201","profile_image_url":"http://a0.twimg.com/profile_images/1178368800/img_normal.jpeg","created_at":"Wed, 26 Jan 2011 03:29:40 +0000","from_user":"tomheon","id_str":"30105060960632832","metadata":{"result_type":"recent"},"to_user_id":128028225,"text":"@chewedwire Great book on Haskell: http://oreilly.com/catalog/9780596514983","id":30105060960632832,"from_user_id":177539201,"to_user":"chewedwire","geo":null,"iso_language_code":"en","to_user_id_str":"128028225","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"782692","profile_image_url":"http://a0.twimg.com/profile_images/1031304589/Profile.2007.1_normal.jpg","created_at":"Wed, 26 Jan 2011 03:29:19 +0000","from_user":"turnageb","id_str":"30104974591533057","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @ovillalon: Paul Ryan solves the mystery of whatever happened to Eddie Haskell. #sotu","id":30104974591533057,"from_user_id":782692,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"128028225","profile_image_url":"http://a1.twimg.com/profile_images/1224994593/Coding_Drunk_normal.jpg","created_at":"Wed, 26 Jan 2011 03:28:04 +0000","from_user":"chewedwire","id_str":"30104657267265536","metadata":{"result_type":"recent"},"to_user_id":177539201,"text":"@tomheon I always loved the pattern matching in SML and it looks like Haskell is MUCH better at it. I'm messing around now at tryhaskell.org","id":30104657267265536,"from_user_id":128028225,"to_user":"tomheon","geo":null,"iso_language_code":"en","to_user_id_str":"177539201","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"12834082","profile_image_url":"http://a3.twimg.com/profile_images/1083036140/mugshot_normal.png","created_at":"Wed, 26 Jan 2011 03:28:01 +0000","from_user":"ovillalon","id_str":"30104647213518848","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Paul Ryan solves the mystery of whatever happened to Eddie Haskell. #sotu","id":30104647213518848,"from_user_id":12834082,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"13540930","profile_image_url":"http://a1.twimg.com/profile_images/1205312219/23467_350321383101_821143101_5114147_3010041_n_normal.jpg","created_at":"Wed, 26 Jan 2011 03:26:02 +0000","from_user":"goodfox","id_str":"30104146455560192","metadata":{"result_type":"recent"},"to_user_id":10226179,"text":"@billykeene22 Bordeaux is one of my heroes. I was so excited when he accepted the invitation to campus. He's been a great friend to Haskell.","id":30104146455560192,"from_user_id":13540930,"to_user":"billykeene22","geo":null,"iso_language_code":"en","to_user_id_str":"10226179","source":"&lt;a href=&quot;http://www.ubertwitter.com/bb/download.php&quot; rel=&quot;nofollow&quot;&gt;\u00dcberTwitter&lt;/a&gt;"},{"from_user_id_str":"18616016","profile_image_url":"http://a2.twimg.com/profile_images/1173522726/214397343_normal.jpg","created_at":"Wed, 26 Jan 2011 03:25:18 +0000","from_user":"josej30","id_str":"30103962313031681","metadata":{"result_type":"recent"},"to_user_id":14870909,"text":"@cris7ian Ahh bueno multiparadigma ya es respetable :) Empezar\u00e9 a explotar la parte funcional de los lenguajes ahora #Haskell","id":30103962313031681,"from_user_id":18616016,"to_user":"Cris7ian","geo":null,"iso_language_code":"es","to_user_id_str":"14870909","source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"14870909","profile_image_url":"http://a2.twimg.com/profile_images/1176930429/oso_yo_normal.png","created_at":"Wed, 26 Jan 2011 03:23:43 +0000","from_user":"Cris7ian","id_str":"30103562360983553","metadata":{"result_type":"recent"},"to_user_id":18616016,"text":"@josej30 hahaha no, es multiparadigma y es bastante lazy. Nothing like haskell, pero s\u00ed, el de Flash","id":30103562360983553,"from_user_id":14870909,"to_user":"josej30","geo":null,"iso_language_code":"es","to_user_id_str":"18616016","source":"&lt;a href=&quot;http://www.echofon.com/&quot; rel=&quot;nofollow&quot;&gt;Echofon&lt;/a&gt;"},{"from_user_id_str":"2421643","profile_image_url":"http://a0.twimg.com/profile_images/1190361665/ernestgrumbles-17_normal.jpg","created_at":"Wed, 26 Jan 2011 03:20:24 +0000","from_user":"ernestgrumbles","id_str":"30102730756333568","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Wow... WolframAlpha did not know who Eddie Haskell is.  Guess I'll never use that &quot;knowledge engine&quot; again.","id":30102730756333568,"from_user_id":2421643,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"128028225","profile_image_url":"http://a1.twimg.com/profile_images/1224994593/Coding_Drunk_normal.jpg","created_at":"Wed, 26 Jan 2011 03:14:21 +0000","from_user":"chewedwire","id_str":"30101204428132352","metadata":{"result_type":"recent"},"to_user_id":177539201,"text":"@tomheon How is Haskell better/different from CL or Scheme? I honestly don't know, although I'm becoming more curious.","id":30101204428132352,"from_user_id":128028225,"to_user":"tomheon","geo":null,"iso_language_code":"en","to_user_id_str":"177539201","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"13540930","profile_image_url":"http://a1.twimg.com/profile_images/1205312219/23467_350321383101_821143101_5114147_3010041_n_normal.jpg","created_at":"Wed, 26 Jan 2011 03:06:54 +0000","from_user":"goodfox","id_str":"30099329809129473","metadata":{"result_type":"recent"},"to_user_id":null,"text":"A day of vision &amp; speeches. #SOTU now. And a wonderful Haskell Convocation address earlier today by Sinte Gleske President Lionel Bordeaux.","id":30099329809129473,"from_user_id":13540930,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.ubertwitter.com/bb/download.php&quot; rel=&quot;nofollow&quot;&gt;\u00dcberTwitter&lt;/a&gt;"},{"from_user_id_str":"119185220","profile_image_url":"http://a0.twimg.com/profile_images/1089027228/dfg_normal.jpg","created_at":"Wed, 26 Jan 2011 03:03:38 +0000","from_user":"LaLiciouz_03","id_str":"30098510623805440","metadata":{"result_type":"recent"},"to_user_id":null,"text":"The Game with the girl room 330 Haskell follow us...","id":30098510623805440,"from_user_id":119185220,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"18616016","profile_image_url":"http://a2.twimg.com/profile_images/1173522726/214397343_normal.jpg","created_at":"Wed, 26 Jan 2011 02:57:25 +0000","from_user":"josej30","id_str":"30096946144215040","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Voy a extra\u00f1ar Haskell cuando regrese al mundo imperativo. Hay alg\u00fan lenguaje imperativo que tenga este poder funcional? #ci3661","id":30096946144215040,"from_user_id":18616016,"geo":null,"iso_language_code":"es","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"17671137","profile_image_url":"http://a2.twimg.com/profile_images/290264834/Haskell-logo-outer-glow_normal.png","created_at":"Wed, 26 Jan 2011 02:55:32 +0000","from_user":"Hackage","id_str":"30096471814574080","metadata":{"result_type":"recent"},"to_user_id":null,"text":"comonad-transformers 0.9.0, added by EdwardKmett: Haskell 98 comonad transformers http://bit.ly/h6xIsf","id":30096471814574080,"from_user_id":17671137,"geo":null,"iso_language_code":"no","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"177539201","profile_image_url":"http://a0.twimg.com/profile_images/1178368800/img_normal.jpeg","created_at":"Wed, 26 Jan 2011 02:48:12 +0000","from_user":"tomheon","id_str":"30094626920603649","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Every time I look at Haskell I love it more.","id":30094626920603649,"from_user_id":177539201,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"60792568","profile_image_url":"http://a0.twimg.com/profile_images/1203647517/glenda-flash_normal.jpg","created_at":"Wed, 26 Jan 2011 02:40:42 +0000","from_user":"r_takaishi","id_str":"30092735935422464","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Haskell\u3088\u308aD\u8a00\u8a9e\u304c\u4e0a\u3068\u306f\u601d\u308f\u306a\u304b\u3063\u305f\uff0e http://www.tiobe.com/index.php/content/paperinfo/tpci/index.html","id":30092735935422464,"from_user_id":60792568,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twmode.sf.net/&quot; rel=&quot;nofollow&quot;&gt;twmode&lt;/a&gt;"},{"from_user_id_str":"160145510","profile_image_url":"http://a0.twimg.com/profile_images/1218108166/going_galt_normal.jpg","created_at":"Wed, 26 Jan 2011 02:39:31 +0000","from_user":"wtp1787","id_str":"30092439653974018","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @KLSouth: Eddie Haskell Goes to Washington...  &quot;You look really nice tonight, Ms Cleaver&quot;","id":30092439653974018,"from_user_id":160145510,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"96616016","profile_image_url":"http://a1.twimg.com/profile_images/1196978169/Picture0002_normal.jpg","created_at":"Wed, 26 Jan 2011 02:39:20 +0000","from_user":"MelissaRNMBA","id_str":"30092392203816960","metadata":{"result_type":"recent"},"to_user_id":14862975,"text":"@KLSouth At least Eddie Haskell was entertaining.","id":30092392203816960,"from_user_id":96616016,"to_user":"KLSouth","geo":null,"iso_language_code":"en","to_user_id_str":"14862975","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"14862975","profile_image_url":"http://a0.twimg.com/profile_images/421596393/kls_4_normal.JPG","created_at":"Wed, 26 Jan 2011 02:38:29 +0000","from_user":"KLSouth","id_str":"30092178327871489","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Eddie Haskell Goes to Washington...  &quot;You look really nice tonight, Ms Cleaver&quot;","id":30092178327871489,"from_user_id":14862975,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"144546280","profile_image_url":"http://a1.twimg.com/a/1295051201/images/default_profile_1_normal.png","created_at":"Wed, 26 Jan 2011 02:36:17 +0000","from_user":"listwarenet","id_str":"30091626869161984","metadata":{"result_type":"recent"},"to_user_id":null,"text":"http://www.listware.net/201101/haskell-beginners/84641-haskell-beginners-wildcards-in-expressions.html Haskell-beginners -  Wild","id":30091626869161984,"from_user_id":144546280,"geo":null,"iso_language_code":"no","to_user_id_str":null,"source":"&lt;a href=&quot;http://1e10.org/cloud/&quot; rel=&quot;nofollow&quot;&gt;1e10&lt;/a&gt;"},{"from_user_id_str":"24538048","profile_image_url":"http://a2.twimg.com/profile_images/1117267605/rope_normal.jpg","created_at":"Wed, 26 Jan 2011 02:23:14 +0000","from_user":"dbph","id_str":"30088341030440960","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @dnene: Skilled Calisthenics. Haskell code that outputs python which spits ruby which emits the haskell source. http://j.mp/YlQUL via @mfeathers","id":30088341030440960,"from_user_id":24538048,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"158951567","profile_image_url":"http://a3.twimg.com/profile_images/962721419/Logo_3_normal.jpg","created_at":"Wed, 26 Jan 2011 01:58:31 +0000","from_user":"YubaVetTech","id_str":"30082124207886336","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Dr. Haskell has just been awarded the prestigious Hayward Award for \u2018Excellence in Education\u2019. This award honors... http://fb.me/QgQCxd74","id":30082124207886336,"from_user_id":158951567,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.facebook.com/twitter&quot; rel=&quot;nofollow&quot;&gt;Facebook&lt;/a&gt;"},{"from_user_id_str":"158951567","profile_image_url":"http://a3.twimg.com/profile_images/962721419/Logo_3_normal.jpg","created_at":"Wed, 26 Jan 2011 01:56:04 +0000","from_user":"YubaVetTech","id_str":"30081505476743168","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Dr. Haskell has just been awarded the prestigous Haward Award for Excellence in Education. This award honors... http://fb.me/PC9mYmCR","id":30081505476743168,"from_user_id":158951567,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.facebook.com/twitter&quot; rel=&quot;nofollow&quot;&gt;Facebook&lt;/a&gt;"},{"from_user_id_str":"2331498","profile_image_url":"http://a3.twimg.com/profile_images/494632120/me_normal.jpg","created_at":"Wed, 26 Jan 2011 01:43:50 +0000","from_user":"Verus","id_str":"30078427155406848","metadata":{"result_type":"recent"},"to_user_id":79273052,"text":"@kami_joe \u3044\u3084\uff0c\u8ab2\u984c\u306f\u89e3\u6c7a\u5bfe\u8c61(\u30d1\u30ba\u30eb\u3068\u304b)\u3092\u4e0e\u3048\u3089\u308c\u3066\uff0c\u554f\u984c\u5b9a\u7fa9\u3068\u30d7\u30ed\u30b0\u30e9\u30df\u30f3\u30b0(\u6307\u5b9a\u8a00\u8a9e\u306fC++\u3082\u3057\u304f\u306fJava)\u3068\u3044\u3046\u3044\u308f\u3070\u666e\u901a\u306a\u8ab2\u984c\u3067\u306f\u3042\u308b\u3093\u3060\u3051\u3069\uff0e\u95a2\u6570\u578b\u8a00\u8a9e\u306fHaskell\u306e\u6388\u696d\u304c\u307e\u305f\u5225\u306b\u3042\u308b\u306e\uff0e","id":30078427155406848,"from_user_id":2331498,"to_user":"kami_joe","geo":null,"iso_language_code":"ja","to_user_id_str":"79273052","source":"&lt;a href=&quot;http://itunes.apple.com/us/app/twitter/id409789998?mt=12&quot; rel=&quot;nofollow&quot;&gt;Twitter for Mac&lt;/a&gt;"},{"from_user_id_str":"79151233","profile_image_url":"http://a2.twimg.com/a/1295051201/images/default_profile_2_normal.png","created_at":"Wed, 26 Jan 2011 01:40:37 +0000","from_user":"cz_newdrafts","id_str":"30077617361125376","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Haskell programming language http://bit.ly/gpPAwB","id":30077617361125376,"from_user_id":79151233,"geo":null,"iso_language_code":"no","to_user_id_str":null,"source":"&lt;a href=&quot;http://tommorris.org/&quot; rel=&quot;nofollow&quot;&gt;tommorris' hacksample&lt;/a&gt;"},{"from_user_id_str":"2331498","profile_image_url":"http://a3.twimg.com/profile_images/494632120/me_normal.jpg","created_at":"Wed, 26 Jan 2011 01:02:57 +0000","from_user":"Verus","id_str":"30068139416879104","metadata":{"result_type":"recent"},"to_user_id":9252720,"text":"@shukukei Java\u3068C\u306f\u3042\u308b\u7a0b\u5ea6\u66f8\u3051\u3066\u3042\u305f\u308a\u307e\u3048\u306a\u3068\u3053\u308d\u304c\u3042\u308b\u304b\u3089\u306a\u30fc\uff0ePython\u306f\u500b\u4eba\u7684\u306b\u611f\u899a\u304c\u5408\u308f\u306a\u3044\uff0e\u611f\u899a\u306a\u306e\u3067\uff0c\u3082\u3046\u3069\u3046\u3057\u3088\u3046\u3082\u306a\u3044\uff57 \u3044\u307e\u306fScala\u3068Haskell\u3092\u3082\u3063\u3068\u6975\u3081\u305f\u3044\u3068\u3053\u308d\uff0e","id":30068139416879104,"from_user_id":2331498,"to_user":"shukukei","geo":null,"iso_language_code":"ja","to_user_id_str":"9252720","source":"&lt;a href=&quot;http://itunes.apple.com/us/app/twitter/id409789998?mt=12&quot; rel=&quot;nofollow&quot;&gt;Twitter for Mac&lt;/a&gt;"},{"from_user_id_str":"2781460","profile_image_url":"http://a0.twimg.com/profile_images/82526625/.joeyicon_normal.jpg","created_at":"Wed, 26 Jan 2011 01:02:35 +0000","from_user":"joeyhess","id_str":"30068046538219520","metadata":{"result_type":"recent"},"to_user_id":null,"text":"just figured out that I can use parameterized types to remove a dependency loop in git-annex's type definitions. whee #haskell","id":30068046538219520,"from_user_id":2781460,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://identi.ca&quot; rel=&quot;nofollow&quot;&gt;identica&lt;/a&gt;"},{"from_user_id_str":"144546280","profile_image_url":"http://a1.twimg.com/a/1295051201/images/default_profile_1_normal.png","created_at":"Wed, 26 Jan 2011 00:54:22 +0000","from_user":"listwarenet","id_str":"30065977920061440","metadata":{"result_type":"recent"},"to_user_id":null,"text":"http://www.listware.net/201101/haskell-beginners/84466-haskell-beginners-bytestring-question.html Haskell-beginners -  Bytestrin","id":30065977920061440,"from_user_id":144546280,"geo":null,"iso_language_code":"no","to_user_id_str":null,"source":"&lt;a href=&quot;http://1e10.org/cloud/&quot; rel=&quot;nofollow&quot;&gt;1e10&lt;/a&gt;"},{"from_user_id_str":"1291845","profile_image_url":"http://a0.twimg.com/profile_images/1225743404/ThinOxygen-small-opaque-solidarity_normal.png","created_at":"Wed, 26 Jan 2011 00:52:07 +0000","from_user":"_aaron_","id_str":"30065412242669568","metadata":{"result_type":"recent"},"to_user_id":null,"text":"wanted: librly licensed high level native compiled lang with min runtime (otherwise cobra/mono would be perfect) for win. lua? haskell? ooc?","id":30065412242669568,"from_user_id":1291845,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"5912444","profile_image_url":"http://a2.twimg.com/profile_images/546261026/ssf0xg11_normal.jpg","created_at":"Wed, 26 Jan 2011 00:45:45 +0000","from_user":"shelarcy","id_str":"30063810773516288","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @Hackage: download 0.3.1.1, added by MagnusTherning: High-level file download based on URLs http://bit.ly/eHfiJB","id":30063810773516288,"from_user_id":5912444,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"135993","profile_image_url":"http://a2.twimg.com/profile_images/1190667086/myface2010small_normal.jpg","created_at":"Wed, 26 Jan 2011 00:41:00 +0000","from_user":"kudzu_naoki","id_str":"30062613287141376","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @tanakh: \u9b54\u6cd5Haskell\u5c11\u5973\u5019\u88dc\u3092\u63a2\u3059\u306e\u3082\u5927\u5909\u306a\u3093\u3060","id":30062613287141376,"from_user_id":135993,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.hootsuite.com&quot; rel=&quot;nofollow&quot;&gt;HootSuite&lt;/a&gt;"},{"from_user_id_str":"52620546","profile_image_url":"http://a1.twimg.com/profile_images/434693356/twitter-icon_normal.png","created_at":"Wed, 26 Jan 2011 00:40:25 +0000","from_user":"omasanori","id_str":"30062465656033280","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u9b54\u6cd5Haskell\u5c11\u5973\u306e\u4e00\u65e5\u306fGHC HEAD\u306e\u30d3\u30eb\u30c9\u304b\u3089\u59cb\u307e\u308b","id":30062465656033280,"from_user_id":52620546,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.movatwi.jp&quot; rel=&quot;nofollow&quot;&gt;www.movatwi.jp&lt;/a&gt;"},{"from_user_id_str":"52620546","profile_image_url":"http://a1.twimg.com/profile_images/434693356/twitter-icon_normal.png","created_at":"Wed, 26 Jan 2011 00:37:12 +0000","from_user":"omasanori","id_str":"30061656331526144","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @tanakh: \u50d5\u3068\u5951\u7d04\u3057\u3066\u9b54\u6cd5Haskell\u5c11\u5973\u306b\u306a\u308d\u3046\u3088\uff01","id":30061656331526144,"from_user_id":52620546,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.hootsuite.com&quot; rel=&quot;nofollow&quot;&gt;HootSuite&lt;/a&gt;"},{"from_user_id_str":"135993","profile_image_url":"http://a2.twimg.com/profile_images/1190667086/myface2010small_normal.jpg","created_at":"Wed, 26 Jan 2011 00:35:42 +0000","from_user":"kudzu_naoki","id_str":"30061282665168896","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @tanakh: \u50d5\u3068\u5951\u7d04\u3057\u3066\u9b54\u6cd5Haskell\u5c11\u5973\u306b\u306a\u308d\u3046\u3088\uff01","id":30061282665168896,"from_user_id":135993,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.hootsuite.com&quot; rel=&quot;nofollow&quot;&gt;HootSuite&lt;/a&gt;"},{"from_user_id_str":"1631333","profile_image_url":"http://a1.twimg.com/profile_images/81268862/profile300_normal.jpg","created_at":"Wed, 26 Jan 2011 00:30:02 +0000","from_user":"qnighy","id_str":"30059855884582913","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @tanakh: \u50d5\u3068\u5951\u7d04\u3057\u3066\u9b54\u6cd5Haskell\u5c11\u5973\u306b\u306a\u308d\u3046\u3088\uff01","id":30059855884582913,"from_user_id":1631333,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.hootsuite.com&quot; rel=&quot;nofollow&quot;&gt;HootSuite&lt;/a&gt;"},{"from_user_id_str":"9093754","profile_image_url":"http://a1.twimg.com/profile_images/99435906/PHD_3d_col_blk_normal.jpg","created_at":"Wed, 26 Jan 2011 00:26:20 +0000","from_user":"phdwine","id_str":"30058925516656640","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Last weekend of Tri Nations Tasting with PHD wines at Haskell Vineyards. Hope you didn't miss the Pinot Noir tasting last week !","id":30058925516656640,"from_user_id":9093754,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"2119923","profile_image_url":"http://a2.twimg.com/profile_images/1096701078/djayprofile_normal.jpg","created_at":"Wed, 26 Jan 2011 00:21:14 +0000","from_user":"djay75","id_str":"30057639543050240","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @dnene: Skilled Calisthenics. Haskell code that outputs python which spits ruby which emits the haskell source. http://j.mp/YlQUL via @mfeathers","id":30057639543050240,"from_user_id":2119923,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"134323731","profile_image_url":"http://a2.twimg.com/profile_images/1225818824/IMG00518-20110125-1547_normal.jpg","created_at":"Wed, 26 Jan 2011 00:19:30 +0000","from_user":"PrinceOfBrkeley","id_str":"30057205554216960","metadata":{"result_type":"recent"},"to_user_id":167093027,"text":"@PayThaPrince go to haskell.","id":30057205554216960,"from_user_id":134323731,"to_user":"PayThaPrince","geo":null,"iso_language_code":"en","to_user_id_str":"167093027","source":"&lt;a href=&quot;http://blackberry.com/twitter&quot; rel=&quot;nofollow&quot;&gt;Twitter for BlackBerry\u00ae&lt;/a&gt;"},{"from_user_id_str":"705857","profile_image_url":"http://a1.twimg.com/profile_images/429483912/ben_twitter_normal.jpg","created_at":"Wed, 26 Jan 2011 00:03:28 +0000","from_user":"bennadel","id_str":"30053166972141569","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Using #Homebrew to install Haskell - the last of the Seven Languages (in Seven Weeks) http://bit.ly/eA9Cv1 This has been some journey.","id":30053166972141569,"from_user_id":705857,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"251466","profile_image_url":"http://a2.twimg.com/profile_images/1194100934/pass_normal.jpg","created_at":"Wed, 26 Jan 2011 00:02:54 +0000","from_user":"simonszu","id_str":"30053025502466048","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Ouh yeah! Ich werde doch produktiv sein, diese Semesterferien. Ich werde funktional Programmieren lernen. #haskell, Baby.","id":30053025502466048,"from_user_id":251466,"geo":null,"iso_language_code":"de","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.echofon.com/&quot; rel=&quot;nofollow&quot;&gt;Echofon&lt;/a&gt;"},{"from_user_id_str":"357786","profile_image_url":"http://a0.twimg.com/profile_images/612044841/FM_2010_normal.jpg","created_at":"Tue, 25 Jan 2011 23:59:13 +0000","from_user":"diligiant","id_str":"30052100973006848","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @paulrbrown: &quot;...we only discovered a single bug after compilation.&quot; (http://t.co/K0wlEYz) #haskell","id":30052100973006848,"from_user_id":357786,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://itunes.apple.com/us/app/twitter/id409789998?mt=12&quot; rel=&quot;nofollow&quot;&gt;Twitter for Mac&lt;/a&gt;"},{"from_user_id_str":"101597523","profile_image_url":"http://a1.twimg.com/profile_images/1206183256/2010-12-12-204902_normal.jpg","created_at":"Tue, 25 Jan 2011 23:54:46 +0000","from_user":"wlad_kent","id_str":"30050977964892160","metadata":{"result_type":"recent"},"to_user_id":86297184,"text":"@Nilson_Neto Isso eh bem basico. quando vc estiver estudando recurs\u00e3o em Haskell, ai vc vai ver oq eh papo de nerd - kkk - 1\u00ba periodo isso.","id":30050977964892160,"from_user_id":101597523,"to_user":"Nilson_Neto","geo":null,"iso_language_code":"pt","to_user_id_str":"86297184","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"144546280","profile_image_url":"http://a1.twimg.com/a/1295051201/images/default_profile_1_normal.png","created_at":"Tue, 25 Jan 2011 23:51:22 +0000","from_user":"listwarenet","id_str":"30050123383832577","metadata":{"result_type":"recent"},"to_user_id":null,"text":"http://www.listware.net/201101/haskell-cafe/84336-haskell-cafe-monomorphic-let-bindings-and-darcs.html Haskell-cafe -  Monomorph","id":30050123383832577,"from_user_id":144546280,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://1e10.org/cloud/&quot; rel=&quot;nofollow&quot;&gt;1e10&lt;/a&gt;"},{"from_user_id_str":"5776901","profile_image_url":"http://a0.twimg.com/profile_images/472682157/lucabw_normal.JPG","created_at":"Tue, 25 Jan 2011 23:48:40 +0000","from_user":"lsbardel","id_str":"30049446469312512","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Very interesting technology stack these guys use http://bit.ly/ghAjS7 #python #redis #haskell","id":30049446469312512,"from_user_id":5776901,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"3005901","profile_image_url":"http://a0.twimg.com/profile_images/278943006/Lone_Pine_Peak_-_Peter_200x200_normal.jpg","created_at":"Tue, 25 Jan 2011 23:45:48 +0000","from_user":"pmonks","id_str":"30048721110573056","metadata":{"result_type":"recent"},"to_user_id":203834278,"text":"@techielicous Haskell: http://bit.ly/gy3oFi  ;-)","id":30048721110573056,"from_user_id":3005901,"to_user":"techielicous","geo":null,"iso_language_code":"en","to_user_id_str":"203834278","source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"162697074","profile_image_url":"http://a2.twimg.com/profile_images/826205838/ryan3_normal.jpg","created_at":"Tue, 25 Jan 2011 23:44:47 +0000","from_user":"TheRonaldMCD","id_str":"30048468781244416","metadata":{"result_type":"recent"},"to_user_id":null,"text":"I'm at Aldi Food Market (4122 Gaston Ave, Haskell, Dallas) http://4sq.com/ekqRY7","id":30048468781244416,"from_user_id":162697074,"geo":null,"iso_language_code":"no","to_user_id_str":null,"source":"&lt;a href=&quot;http://foursquare.com&quot; rel=&quot;nofollow&quot;&gt;foursquare&lt;/a&gt;"},{"from_user_id_str":"203834278","profile_image_url":"http://a0.twimg.com/profile_images/1223375080/me_normal.jpg","created_at":"Tue, 25 Jan 2011 23:44:04 +0000","from_user":"techielicous","id_str":"30048286589067264","metadata":{"result_type":"recent"},"to_user_id":3005901,"text":"@pmonks I was thinking Haskell or Scheme","id":30048286589067264,"from_user_id":203834278,"to_user":"pmonks","geo":null,"iso_language_code":"en","to_user_id_str":"3005901","source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"537690","profile_image_url":"http://a3.twimg.com/profile_images/45665122/nushiostamp_normal.jpg","created_at":"Tue, 25 Jan 2011 23:38:14 +0000","from_user":"nushio","id_str":"30046818658164737","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u305d\u3046\u3044\u3084\u5951\u7d04\u306b\u3088\u308b\u30d7\u30ed\u30b0\u30e9\u30df\u30f3\u30b0\u3068\u304b\u3042\u3063\u305f\u306a\u3002Haskell\u306a\u3093\u304b\u3088\u308a\u3042\u3063\u3061\u306e\u65b9\u304c\u9b54\u6cd5\u5c11\u5973\u547c\u3070\u308f\u308a\u306b\u3075\u3055\u308f\u3057\u3044\u3002","id":30046818658164737,"from_user_id":537690,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"181192","profile_image_url":"http://a2.twimg.com/profile_images/1089890701/q530888600_9828_normal.jpg","created_at":"Tue, 25 Jan 2011 23:38:03 +0000","from_user":"aodag","id_str":"30046772223016960","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @omasanori: \u300c\u3082\u3046\u4f55\u5e74\u3082Haskell\u306e\u3053\u3068\u3057\u304b\u8003\u3048\u3066\u306a\u304b\u3063\u305f\u304b\u3089\u306a\u3001\u30eb\u30fc\u30d7\u306e\u4f7f\u3044\u65b9\u5fd8\u308c\u3066\u305f\u300d","id":30046772223016960,"from_user_id":181192,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.movatwi.jp&quot; rel=&quot;nofollow&quot;&gt;www.movatwi.jp&lt;/a&gt;"},{"from_user_id_str":"5886055","profile_image_url":"http://a3.twimg.com/profile_images/85531669/ichi_normal.jpg","created_at":"Tue, 25 Jan 2011 23:37:53 +0000","from_user":"kanaya","id_str":"30046729243983873","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u9e97\u3057\u3044\u3002\u201c@omasanori: \u300c\u3082\u3046\u4f55\u5e74\u3082Haskell\u306e\u3053\u3068\u3057\u304b\u8003\u3048\u3066\u306a\u304b\u3063\u305f\u304b\u3089\u306a\u3001\u30eb\u30fc\u30d7\u306e\u4f7f\u3044\u65b9\u5fd8\u308c\u3066\u305f\u300d\u201d","id":30046729243983873,"from_user_id":5886055,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://itunes.apple.com/app/twitter/id333903271?mt=8&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPad&lt;/a&gt;"},{"from_user_id_str":"52620546","profile_image_url":"http://a1.twimg.com/profile_images/434693356/twitter-icon_normal.png","created_at":"Tue, 25 Jan 2011 23:36:26 +0000","from_user":"omasanori","id_str":"30046367187476481","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u300c\u3082\u3046\u4f55\u5e74\u3082Haskell\u306e\u3053\u3068\u3057\u304b\u8003\u3048\u3066\u306a\u304b\u3063\u305f\u304b\u3089\u306a\u3001\u30eb\u30fc\u30d7\u306e\u4f7f\u3044\u65b9\u5fd8\u308c\u3066\u305f\u300d","id":30046367187476481,"from_user_id":52620546,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.movatwi.jp&quot; rel=&quot;nofollow&quot;&gt;www.movatwi.jp&lt;/a&gt;"},{"from_user_id_str":"1659992","profile_image_url":"http://a1.twimg.com/profile_images/504488750/1_normal.jpg","created_at":"Tue, 25 Jan 2011 23:16:13 +0000","from_user":"finalfusion","id_str":"30041278544609280","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @necocen: Haskell\u306e\u30df\u30b5\u30ef\u3001\u3068\u3044\u3046\u3082\u306e\u3092\u8003\u3048\u3066\u307f\u3066\u3071\u3063\u3068\u3057\u306a\u3044","id":30041278544609280,"from_user_id":1659992,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"272513","profile_image_url":"http://a2.twimg.com/profile_images/320244078/Mike_Painting2_normal.png","created_at":"Tue, 25 Jan 2011 23:05:09 +0000","from_user":"mikehadlow","id_str":"30038492373319680","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Just written the same simple program in C#, F# and Haskell: 18, 12 and 9 LoC respectively. And I'm much better at C# than F# or Haskell.","id":30038492373319680,"from_user_id":272513,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"89393264","profile_image_url":"http://a0.twimg.com/profile_images/1169486472/020_portland_me_normal.jpg","created_at":"Tue, 25 Jan 2011 23:01:28 +0000","from_user":"newsportlandme","id_str":"30037564589084673","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Governor's Choice to Run Maine DOC Under Scrutiny - MPBN: State Rep. Anne Haskell, a Portland Democrat, says Mai... http://bit.ly/fO30gC","id":30037564589084673,"from_user_id":89393264,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"38138","profile_image_url":"http://a3.twimg.com/profile_images/1212565885/Screen_shot_2011-01-11_at_5.28.47_PM_normal.png","created_at":"Tue, 25 Jan 2011 22:59:55 +0000","from_user":"michaelneale","id_str":"30037174615281664","metadata":{"result_type":"recent"},"to_user_id":null,"text":"The haskell EvilMangler http://t.co/P2Tls7J","id":30037174615281664,"from_user_id":38138,"geo":null,"iso_language_code":"no","to_user_id_str":null,"source":"&lt;a href=&quot;http://itunes.apple.com/us/app/twitter/id409789998?mt=12&quot; rel=&quot;nofollow&quot;&gt;Twitter for Mac&lt;/a&gt;"},{"from_user_id_str":"45692","profile_image_url":"http://a3.twimg.com/profile_images/1177741507/dogkarno_r_normal.jpg","created_at":"Tue, 25 Jan 2011 22:45:06 +0000","from_user":"karno","id_str":"30033448286552064","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Haskell\u3063\u3066\u95a2\u6570\u306b\u30ab\u30ec\u30fc\u7c89\u3076\u3061\u8fbc\u3080\u3068\u304b\u306a\u3093\u3068\u304b","id":30033448286552064,"from_user_id":45692,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://yubitter.com/&quot; rel=&quot;nofollow&quot;&gt;yubitter&lt;/a&gt;"},{"from_user_id_str":"27605","profile_image_url":"http://a2.twimg.com/profile_images/15826632/meron_normal.jpg","created_at":"Tue, 25 Jan 2011 22:44:23 +0000","from_user":"celeron1ghz","id_str":"30033268761956352","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @necocen: Haskell\u306e\u30df\u30b5\u30ef\u3001\u3068\u3044\u3046\u3082\u306e\u3092\u8003\u3048\u3066\u307f\u3066\u3071\u3063\u3068\u3057\u306a\u3044","id":30033268761956352,"from_user_id":27605,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"12901","profile_image_url":"http://a1.twimg.com/profile_images/56091349/haruhi_nagato-Y_normal.jpg","created_at":"Tue, 25 Jan 2011 22:43:30 +0000","from_user":"necocen","id_str":"30033044035346432","metadata":{"result_type":"recent"},"to_user_id":null,"text":"\u307e\u3042Haskell\u77e5\u3089\u3093\u3057\u306d","id":30033044035346432,"from_user_id":12901,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"12901","profile_image_url":"http://a1.twimg.com/profile_images/56091349/haruhi_nagato-Y_normal.jpg","created_at":"Tue, 25 Jan 2011 22:42:54 +0000","from_user":"necocen","id_str":"30032894856531968","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Haskell\u306e\u30df\u30b5\u30ef\u3001\u3068\u3044\u3046\u3082\u306e\u3092\u8003\u3048\u3066\u307f\u3066\u3071\u3063\u3068\u3057\u306a\u3044","id":30032894856531968,"from_user_id":12901,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"10821270","profile_image_url":"http://a0.twimg.com/profile_images/1195474259/Salto_Marlon_normal.jpg","created_at":"Tue, 25 Jan 2011 22:35:09 +0000","from_user":"jjedMoriAnktah","id_str":"30030942106025984","metadata":{"result_type":"recent"},"to_user_id":null,"text":"&quot;A Haskell program that outputs a Python program that outputs a Ruby program that outputs the original Haskell program&quot; http://is.gd/E6Julu","id":30030942106025984,"from_user_id":10821270,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"7554762","profile_image_url":"http://a0.twimg.com/profile_images/100515212/ajay-photo_normal.jpg","created_at":"Tue, 25 Jan 2011 22:34:30 +0000","from_user":"comatose_kid","id_str":"30030780205899776","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @bumptech: Bump Dev Blog - Why we use Haskell at Bump http://devblog.bu.mp/haskell-at-bump","id":30030780205899776,"from_user_id":7554762,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"148474705","profile_image_url":"http://a0.twimg.com/profile_images/1119234716/fur_hat_-_gilbeys_vodka_-_life_-_11-30-1962_normal.JPG","created_at":"Tue, 25 Jan 2011 22:29:19 +0000","from_user":"landmvintage","id_str":"30029476003840000","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @faerymoongodess: Magnificent Miriam Haskell Baroque Pearl Necklace by @MercyMadge http://etsy.me/hqRl51","id":30029476003840000,"from_user_id":148474705,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"37715508","profile_image_url":"http://a1.twimg.com/profile_images/1207891411/ballerina_normal.jpg","created_at":"Tue, 25 Jan 2011 22:24:52 +0000","from_user":"faerymoongodess","id_str":"30028356326002688","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Magnificent Miriam Haskell Baroque Pearl Necklace by @MercyMadge http://etsy.me/hqRl51","id":30028356326002688,"from_user_id":37715508,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"17671137","profile_image_url":"http://a2.twimg.com/profile_images/290264834/Haskell-logo-outer-glow_normal.png","created_at":"Tue, 25 Jan 2011 22:17:16 +0000","from_user":"Hackage","id_str":"30026442456694784","metadata":{"result_type":"recent"},"to_user_id":null,"text":"base16-bytestring 0.1.0.0, added by BryanOSullivan: Fast base16 (hex) encoding and deconding for ByteStrings http://bit.ly/eu75TN","id":30026442456694784,"from_user_id":17671137,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"199750997","profile_image_url":"http://a3.twimg.com/sticky/default_profile_images/default_profile_2_normal.png","created_at":"Tue, 25 Jan 2011 22:12:07 +0000","from_user":"benreads","id_str":"30025146991382528","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Writing Systems Software in a Functional Language -- brief, but fun. I want to see how well they can make Systems Haskell perform at scale.","id":30025146991382528,"from_user_id":199750997,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"35359797","profile_image_url":"http://a1.twimg.com/profile_images/1173453785/48893_521140819_1896062_q_normal.jpg","created_at":"Tue, 25 Jan 2011 22:09:45 +0000","from_user":"jcawthorne1","id_str":"30024549265309696","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Just saw Jeff Haskell give the middle finger!! Lol awesome","id":30024549265309696,"from_user_id":35359797,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"13540930","profile_image_url":"http://a1.twimg.com/profile_images/1205312219/23467_350321383101_821143101_5114147_3010041_n_normal.jpg","created_at":"Tue, 25 Jan 2011 22:06:44 +0000","from_user":"goodfox","id_str":"30023790381498369","metadata":{"result_type":"recent"},"to_user_id":null,"text":"At the Haskell Convocation. (@ Haskell Indian Nations U Auditorium) http://4sq.com/eKKXyn","id":30023790381498369,"from_user_id":13540930,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://foursquare.com&quot; rel=&quot;nofollow&quot;&gt;foursquare&lt;/a&gt;"},{"from_user_id_str":"14674418","profile_image_url":"http://a2.twimg.com/profile_images/238230125/IMG_1030-1_normal.jpg","created_at":"Tue, 25 Jan 2011 22:02:31 +0000","from_user":"sanityinc","id_str":"30022731919523840","metadata":{"result_type":"recent"},"to_user_id":371289,"text":"@xshay Haskell's great, but Clojure's similarly lazy in all the ways that matter, and is more practical for day-to-day use.","id":30022731919523840,"from_user_id":14674418,"to_user":"xshay","geo":null,"iso_language_code":"en","to_user_id_str":"371289","source":"&lt;a href=&quot;http://www.echofon.com/&quot; rel=&quot;nofollow&quot;&gt;Echofon&lt;/a&gt;"},{"from_user_id_str":"371289","profile_image_url":"http://a2.twimg.com/profile_images/1113482439/me-brisbane_normal.jpg","created_at":"Tue, 25 Jan 2011 21:58:29 +0000","from_user":"xshay","id_str":"30021714444292096","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Just made a lazy sequence of prime numbers in haskell. I'm smitten.","id":30021714444292096,"from_user_id":371289,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://itunes.apple.com/us/app/twitter/id409789998?mt=12&quot; rel=&quot;nofollow&quot;&gt;Twitter for Mac&lt;/a&gt;"},{"from_user_id_str":"199453873","profile_image_url":"http://a3.twimg.com/a/1294785484/images/default_profile_3_normal.png","created_at":"Tue, 25 Jan 2011 21:48:40 +0000","from_user":"stackfeed","id_str":"30019243982454784","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Practical Scala reference manual, for searching things like method names: Hello, \n\nInspired by\nHaskell API Searc... http://bit.ly/g4zWZQ","id":30019243982454784,"from_user_id":199453873,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"17489366","profile_image_url":"http://a3.twimg.com/sticky/default_profile_images/default_profile_2_normal.png","created_at":"Tue, 25 Jan 2011 21:45:32 +0000","from_user":"aapnoot","id_str":"30018455189065728","metadata":{"result_type":"recent"},"to_user_id":null,"text":"download 0.3.1, added by MagnusTherning: High-level file download based on URLs http://bit.ly/i8mYvi","id":30018455189065728,"from_user_id":17489366,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"17489366","profile_image_url":"http://a3.twimg.com/sticky/default_profile_images/default_profile_2_normal.png","created_at":"Tue, 25 Jan 2011 21:45:31 +0000","from_user":"aapnoot","id_str":"30018452601180160","metadata":{"result_type":"recent"},"to_user_id":null,"text":"download 0.3.1.1, added by MagnusTherning: High-level file download based on URLs http://bit.ly/eHfiJB","id":30018452601180160,"from_user_id":17489366,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"17671137","profile_image_url":"http://a2.twimg.com/profile_images/290264834/Haskell-logo-outer-glow_normal.png","created_at":"Tue, 25 Jan 2011 21:45:31 +0000","from_user":"Hackage","id_str":"30018451867176960","metadata":{"result_type":"recent"},"to_user_id":null,"text":"download 0.3.1, added by MagnusTherning: High-level file download based on URLs http://bit.ly/i8mYvi","id":30018451867176960,"from_user_id":17671137,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"138502705","profile_image_url":"http://a1.twimg.com/profile_images/1083845614/yclogo_normal.gif","created_at":"Tue, 25 Jan 2011 21:45:04 +0000","from_user":"newsyc100","id_str":"30018340839755777","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Haskell improves log processing 4x over Python http://devblog.bu.mp/haskell-at-bump (http://bit.ly/gQxxR8)","id":30018340839755777,"from_user_id":138502705,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://news.ycombinator.com&quot; rel=&quot;nofollow&quot;&gt;newsyc&lt;/a&gt;"},{"from_user_id_str":"1578246","profile_image_url":"http://a0.twimg.com/profile_images/59312315/avatar_simpson_small_normal.jpg","created_at":"Tue, 25 Jan 2011 21:43:32 +0000","from_user":"magthe","id_str":"30017953801969665","metadata":{"result_type":"recent"},"to_user_id":null,"text":"New version of download uploaded to #hackage #haskell","id":30017953801969665,"from_user_id":1578246,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://api.supertweet.net&quot; rel=&quot;nofollow&quot;&gt;MyAuth API Proxy&lt;/a&gt;"},{"from_user_id_str":"135838970","profile_image_url":"http://a2.twimg.com/profile_images/1079929891/logo_normal.png","created_at":"Tue, 25 Jan 2011 21:37:51 +0000","from_user":"reward999","id_str":"30016525180084224","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Found Great Dane (Main &amp; Haskell- Dallas): We found a great dane. 214-712-0000 http://bit.ly/fQ8iBw","id":30016525180084224,"from_user_id":135838970,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"137719265","profile_image_url":"http://a2.twimg.com/profile_images/1092224079/8__4__normal.jpg","created_at":"Tue, 25 Jan 2011 21:34:22 +0000","from_user":"WeiMatas","id_str":"30015646020411392","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Fourth day Rez party Eddie Haskell County in second life \u2013 which took place in cabaret Tadd","id":30015646020411392,"from_user_id":137719265,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://dlvr.it&quot; rel=&quot;nofollow&quot;&gt;dlvr.it&lt;/a&gt;"},{"from_user_id_str":"144546280","profile_image_url":"http://a1.twimg.com/a/1295051201/images/default_profile_1_normal.png","created_at":"Tue, 25 Jan 2011 21:33:21 +0000","from_user":"listwarenet","id_str":"30015392571195392","metadata":{"result_type":"recent"},"to_user_id":null,"text":"http://www.listware.net/201101/haskell-cafe/83889-haskell-cafe-gpl-license-of-h-matrix-and-prelude-numeric.html Haskell-cafe -","id":30015392571195392,"from_user_id":144546280,"geo":null,"iso_language_code":"no","to_user_id_str":null,"source":"&lt;a href=&quot;http://1e10.org/cloud/&quot; rel=&quot;nofollow&quot;&gt;1e10&lt;/a&gt;"}],"max_id":30121530767708160,"since_id":0,"refresh_url":"?since_id=30121530767708160&q=haskell","next_page":"?page=2&max_id=30121530767708160&rpp=100&q=haskell","results_per_page":100,"page":1,"completed_in":1.195569,"since_id_str":"0","max_id_str":"30121530767708160","query":"haskell"}
+ test/json-data/twitter20.json view
@@ -0,0 +1,1 @@+{"results":[{"from_user_id_str":"166199691","profile_image_url":"http://a2.twimg.com/profile_images/1252958188/38_normal.jpg","created_at":"Fri, 25 Feb 2011 17:41:26 +0000","from_user":"_classicc","id_str":"41191052790603776","metadata":{"result_type":"recent"},"to_user_id":null,"text":"my twitter is actin slow today.","id":41191052790603776,"from_user_id":166199691,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"138835410","profile_image_url":"http://a1.twimg.com/profile_images/1250985094/243321801_normal.jpg","created_at":"Fri, 25 Feb 2011 17:41:26 +0000","from_user":"amereronday","id_str":"41191050307567616","metadata":{"result_type":"recent"},"to_user_id":null,"text":"twitter jail, here i come.","id":41191050307567616,"from_user_id":138835410,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"359548","profile_image_url":"http://a2.twimg.com/profile_images/53612334/don_otvos_normal.jpg","created_at":"Fri, 25 Feb 2011 17:41:26 +0000","from_user":"donnyo","id_str":"41191050110451712","metadata":{"result_type":"recent"},"to_user_id":7074534,"text":"@stlsmallbiz I see there is currently a Twitter promo too....tempting....","id":41191050110451712,"from_user_id":359548,"to_user":"stlsmallbiz","geo":null,"iso_language_code":"en","place":{"id":"82b7b2f97b12261d","type_":"poi","full_name":"Yammer Inc, San Francisco"},"to_user_id_str":"7074534","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"122523770","profile_image_url":"http://a0.twimg.com/profile_images/1073533262/11646_1076222405045_1810798478_156114_3663737_n_normal.jpg","created_at":"Fri, 25 Feb 2011 17:41:26 +0000","from_user":"msBreChan","id_str":"41191049720238080","metadata":{"result_type":"recent"},"to_user_id":null,"text":"so twitter gt spam now -_-","id":41191049720238080,"from_user_id":122523770,"geo":{"type_":"Point","coordinates":[35.2213,-80.8276]},"iso_language_code":"en","place":{"id":"4d5ed95f830e9b41","type_":"neighborhood","full_name":"Elizabeth, Charlotte"},"to_user_id_str":null,"source":"&lt;a href=&quot;http://mobile.twitter.com&quot; rel=&quot;nofollow&quot;&gt;Twitter for Android&lt;/a&gt;"},{"from_user_id_str":"229024950","profile_image_url":"http://a3.twimg.com/sticky/default_profile_images/default_profile_6_normal.png","created_at":"Fri, 25 Feb 2011 17:41:25 +0000","from_user":"NinaMaechik","id_str":"41191047853912064","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Glad I can remember my twitter password!  LOL!  Hugs to Nick from his aunties...","id":41191047853912064,"from_user_id":229024950,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"215545822","profile_image_url":"http://a2.twimg.com/profile_images/1239909540/RTL_normal.png","created_at":"Fri, 25 Feb 2011 17:41:25 +0000","from_user":"RightToLaugh","id_str":"41191047371558912","metadata":{"result_type":"recent"},"to_user_id":null,"text":"#Bacon wrapped dates http://bit.ly/hUxVC9 Just like my grandma used to make http://twitter.com/#","id":41191047371558912,"from_user_id":215545822,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"217845526","profile_image_url":"http://a0.twimg.com/sticky/default_profile_images/default_profile_3_normal.png","created_at":"Fri, 25 Feb 2011 17:41:25 +0000","from_user":"TheShoeLooker","id_str":"41191047052804096","metadata":{"result_type":"recent"},"to_user_id":38033240,"text":"@ShoeDazzle love love love to twitter or blog about your deals and shoes!\ntheshoelooker.blogspot.com","id":41191047052804096,"from_user_id":217845526,"to_user":"shoedazzle","geo":null,"iso_language_code":"en","to_user_id_str":"38033240","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"24953438","profile_image_url":"http://a3.twimg.com/profile_images/1181252763/76820_10150100014748783_677998782_7386695_4377006_n_normal.jpg","created_at":"Fri, 25 Feb 2011 17:41:25 +0000","from_user":"liljermaine32","id_str":"41191046025056256","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Twitter fam I gotta question 4 ya is texas south or midwest #arguments","id":41191046025056256,"from_user_id":24953438,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://mobile.twitter.com&quot; rel=&quot;nofollow&quot;&gt;Twitter for Android&lt;/a&gt;"},{"from_user_id_str":"122451024","profile_image_url":"http://a2.twimg.com/profile_images/1143400129/100706-ETCanada_201134_1__normal.jpg","created_at":"Fri, 25 Feb 2011 17:41:25 +0000","from_user":"Kim_DEon","id_str":"41191045161164801","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Help @CARE @EvaLongoria and ME use Twitter to change the lives of girls in poverty across the world! Ready? Go to http://TwitChange.com NOW!","id":41191045161164801,"from_user_id":122451024,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"128351479","profile_image_url":"http://a3.twimg.com/profile_images/1150198829/Kirby_Photo__edit__normal.JPG","created_at":"Fri, 25 Feb 2011 17:41:24 +0000","from_user":"TheDaveKirby","id_str":"41191043894493184","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Maybe its time to clean house...MY HOUSE http://bit.ly/ejG22P","id":41191043894493184,"from_user_id":128351479,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"87512773","profile_image_url":"http://a0.twimg.com/profile_images/1252511106/image_normal.jpg","created_at":"Fri, 25 Feb 2011 17:41:24 +0000","from_user":"supportthehood","id_str":"41191043206610946","metadata":{"result_type":"recent"},"to_user_id":2467330,"text":"@eastcoastmp3 #FF  it's the way twitter works !!!","id":41191043206610946,"from_user_id":87512773,"to_user":"eastcoastmp3","geo":null,"iso_language_code":"en","to_user_id_str":"2467330","source":"&lt;a href=&quot;http://twitter.com/&quot; rel=&quot;nofollow&quot;&gt;Twitter for iPhone&lt;/a&gt;"},{"from_user_id_str":"149894545","profile_image_url":"http://a0.twimg.com/profile_images/1219334400/patrick_camo_normal.jpg","created_at":"Fri, 25 Feb 2011 17:41:24 +0000","from_user":"suburbpat","id_str":"41191040912330752","metadata":{"result_type":"recent"},"to_user_id":null,"text":"I havent had a rib session on twitter in a while.......I wanna Rib with a nigga with alot followers lol","id":41191040912330752,"from_user_id":149894545,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"97749622","profile_image_url":"http://a0.twimg.com/profile_images/1254640781/IMG1633A_normal.jpg","created_at":"Fri, 25 Feb 2011 17:41:24 +0000","from_user":"jrricardosantos","id_str":"41191040547430400","metadata":{"result_type":"recent"},"to_user_id":101017476,"text":"@yofzs pq tirou o CAPS LOCK? kkkk tava bem legal...hehehe eu estou no t\u00e9dio...kkk tbm estou no twitter e msn..aff!!","id":41191040547430400,"from_user_id":97749622,"to_user":"yofzs","geo":null,"iso_language_code":"en","to_user_id_str":"101017476","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"94381536","profile_image_url":"http://a1.twimg.com/profile_images/1174777929/41509_100001699300415_3521888_n_normal.jpg","created_at":"Fri, 25 Feb 2011 17:41:23 +0000","from_user":"DonNieves","id_str":"41191038978752512","metadata":{"result_type":"recent"},"to_user_id":151926400,"text":"@TiiH13 cheira meu ovo, no email, no orkut, no msn, no facebook, no twitter, no skype, no spark, no ICQ e no google talk","id":41191038978752512,"from_user_id":94381536,"to_user":"TiiH13","geo":null,"iso_language_code":"en","to_user_id_str":"151926400","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"211858432","profile_image_url":"http://a3.twimg.com/profile_images/1231906180/JonasBrothers_normal.jpg","created_at":"Fri, 25 Feb 2011 17:41:23 +0000","from_user":"OriginalJBfans","id_str":"41191038328651776","metadata":{"result_type":"recent"},"to_user_id":null,"text":"I literally forgot about this twitter... so whats occuring followers?","id":41191038328651776,"from_user_id":211858432,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"90317132","profile_image_url":"http://a3.twimg.com/profile_images/1230085056/254101635_normal.jpg","created_at":"Fri, 25 Feb 2011 17:41:23 +0000","from_user":"Just2smooth","id_str":"41191036936126464","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Wassup twitter","id":41191036936126464,"from_user_id":90317132,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://ubersocial.com&quot; rel=&quot;nofollow&quot;&gt;\u00dcberSocial&lt;/a&gt;"},{"from_user_id_str":"144771504","profile_image_url":"http://a1.twimg.com/profile_images/1243343836/image_normal.jpg","created_at":"Fri, 25 Feb 2011 17:41:23 +0000","from_user":"ima_b_b_badman","id_str":"41191036638339072","metadata":{"result_type":"recent"},"to_user_id":null,"text":"i been M.I.A. all day twitter my bad","id":41191036638339072,"from_user_id":144771504,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"207922664","profile_image_url":"http://a2.twimg.com/profile_images/1237106108/258356167_normal.jpg","created_at":"Fri, 25 Feb 2011 17:41:22 +0000","from_user":"itsthecarter_","id_str":"41191036013391873","metadata":{"result_type":"recent"},"to_user_id":null,"text":"twitter is a stupid addiction.","id":41191036013391873,"from_user_id":207922664,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://ubersocial.com&quot; rel=&quot;nofollow&quot;&gt;\u00dcberSocial&lt;/a&gt;"},{"from_user_id_str":"150000649","profile_image_url":"http://a0.twimg.com/profile_images/1237789059/belllaa_normal.jpg","created_at":"Fri, 25 Feb 2011 17:41:22 +0000","from_user":"angelicabrvo","id_str":"41191035707207680","metadata":{"result_type":"recent"},"to_user_id":null,"text":"que hubo twitter? huy que feoxd","id":41191035707207680,"from_user_id":150000649,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://blackberry.com/twitter&quot; rel=&quot;nofollow&quot;&gt;Twitter for BlackBerry\u00ae&lt;/a&gt;"},{"from_user_id_str":"185713003","profile_image_url":"http://a2.twimg.com/profile_images/1191421540/163229_177015295661467_154687431227587_492170_2147500_n_normal.jpg","created_at":"Fri, 25 Feb 2011 17:41:22 +0000","from_user":"downbytheshores","id_str":"41191033945595905","metadata":{"result_type":"recent"},"to_user_id":null,"text":"follow us on twitter\nwww.twitter.com/downbytheshores http://fb.me/DhDS915e","id":41191033945595905,"from_user_id":185713003,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.facebook.com/twitter&quot; rel=&quot;nofollow&quot;&gt;Facebook&lt;/a&gt;"}],"max_id":41191052790603776,"since_id":38643906774044672,"refresh_url":"?since_id=41191052790603776&q=twitter","next_page":"?page=2&max_id=41191052790603776&rpp=20&lang=en&q=twitter","results_per_page":20,"page":1,"completed_in":0.128719,"warning":"adjusted since_id to 38643906774044672 (), requested since_id was older than allowed -- since_id removed for pagination.","since_id_str":"38643906774044672","max_id_str":"41191052790603776","query":"twitter"}
+ test/json-data/twitter50.json view
@@ -0,0 +1,1 @@+{"results":[{"from_user_id_str":"207858021","profile_image_url":"http://a3.twimg.com/sticky/default_profile_images/default_profile_2_normal.png","created_at":"Wed, 26 Jan 2011 04:30:38 +0000","from_user":"pboudarga","id_str":"30120402839666689","metadata":{"result_type":"recent"},"to_user_id":null,"text":"I'm at Rolla Sushi Grill (27737 Bouquet Canyon Road, #106, Btw Haskell Canyon and Rosedell Drive, Saugus) http://4sq.com/gqqdhs","id":30120402839666689,"from_user_id":207858021,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://foursquare.com&quot; rel=&quot;nofollow&quot;&gt;foursquare&lt;/a&gt;"},{"from_user_id_str":"69988683","profile_image_url":"http://a0.twimg.com/profile_images/1211955817/avatar_7888_normal.gif","created_at":"Wed, 26 Jan 2011 04:25:23 +0000","from_user":"YNK33","id_str":"30119083059978240","metadata":{"result_type":"recent"},"to_user_id":null,"text":"hsndfile 0.5.0: Free and open source Haskell bindings for libsndfile http://bit.ly/gHaBWG Mac Os","id":30119083059978240,"from_user_id":69988683,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"81492","profile_image_url":"http://a1.twimg.com/profile_images/423894208/Picture_7_normal.jpg","created_at":"Wed, 26 Jan 2011 04:24:28 +0000","from_user":"satzz","id_str":"30118851488251904","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Emacs\u306e\u30e2\u30fc\u30c9\u8868\u793a\u304c\u4eca(Ruby Controller Outputz RoR Flymake REl hs)\u3068\u306a\u3063\u3066\u3066\u3088\u304f\u308f\u304b\u3089\u306a\u3044\u3093\u3060\u3051\u3069\u6700\u5f8c\u306eREl\u3068\u304bhs\u3063\u3066\u4f55\u3060\u308d\u3046\u2026haskell\u3068\u304b2\u5e74\u4ee5\u4e0a\u66f8\u3044\u3066\u306a\u3044\u3051\u3069\u2026","id":30118851488251904,"from_user_id":81492,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.hootsuite.com&quot; rel=&quot;nofollow&quot;&gt;HootSuite&lt;/a&gt;"},{"from_user_id_str":"9518356","profile_image_url":"http://a2.twimg.com/profile_images/119165723/ocaml-icon_normal.png","created_at":"Wed, 26 Jan 2011 04:19:19 +0000","from_user":"planet_ocaml","id_str":"30117557788741632","metadata":{"result_type":"recent"},"to_user_id":null,"text":"I so miss #haskell type classes in #ocaml - i want to do something like refinement. Also why does ocaml not have... http://bit.ly/geYRwt","id":30117557788741632,"from_user_id":9518356,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"218059","profile_image_url":"http://a1.twimg.com/profile_images/1053837723/twitter-icon9_normal.jpg","created_at":"Wed, 26 Jan 2011 04:16:32 +0000","from_user":"aprikip","id_str":"30116854940835840","metadata":{"result_type":"recent"},"to_user_id":null,"text":"yatex-mode\u3084haskell-mode\u306e\u3053\u3068\u3067\u3059\u306d\u3001\u308f\u304b\u308a\u307e\u3059\u3002","id":30116854940835840,"from_user_id":218059,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://sites.google.com/site/yorufukurou/&quot; rel=&quot;nofollow&quot;&gt;YoruFukurou&lt;/a&gt;"},{"from_user_id_str":"216363","profile_image_url":"http://a1.twimg.com/profile_images/72454310/Tim-Avatar_normal.png","created_at":"Wed, 26 Jan 2011 04:15:30 +0000","from_user":"dysinger","id_str":"30116594684264448","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Haskell in Hawaii tonight for me... #fun","id":30116594684264448,"from_user_id":216363,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.nambu.com/&quot; rel=&quot;nofollow&quot;&gt;Nambu&lt;/a&gt;"},{"from_user_id_str":"1774820","profile_image_url":"http://a2.twimg.com/profile_images/61169291/dan_desert_thumb_normal.jpg","created_at":"Wed, 26 Jan 2011 04:13:36 +0000","from_user":"DanMil","id_str":"30116117682851840","metadata":{"result_type":"recent"},"to_user_id":1594784,"text":"@ojrac @chewedwire @tomheon Haskell isn't a language, it's a belief system.  A seductive one...","id":30116117682851840,"from_user_id":1774820,"to_user":"ojrac","geo":null,"iso_language_code":"en","to_user_id_str":"1594784","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"659256","profile_image_url":"http://a0.twimg.com/profile_images/746976711/angular-final_normal.jpg","created_at":"Wed, 26 Jan 2011 04:11:06 +0000","from_user":"djspiewak","id_str":"30115488931520512","metadata":{"result_type":"recent"},"to_user_id":null,"text":"One of the very nice things about Haskell as opposed to SML is the reduced proliferation of identifiers (e.g. andb, orb, etc). #typeclasses","id":30115488931520512,"from_user_id":659256,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://itunes.apple.com/us/app/twitter/id409789998?mt=12&quot; rel=&quot;nofollow&quot;&gt;Twitter for Mac&lt;/a&gt;"},{"from_user_id_str":"144546280","profile_image_url":"http://a1.twimg.com/a/1295051201/images/default_profile_1_normal.png","created_at":"Wed, 26 Jan 2011 04:06:12 +0000","from_user":"listwarenet","id_str":"30114255890026496","metadata":{"result_type":"recent"},"to_user_id":null,"text":"http://www.listware.net/201101/haskell-cafe/84752-re-haskell-cafe-gpl-license-of-h-matrix-and-prelude-numeric.html Re: Haskell-c","id":30114255890026496,"from_user_id":144546280,"geo":null,"iso_language_code":"no","to_user_id_str":null,"source":"&lt;a href=&quot;http://1e10.org/cloud/&quot; rel=&quot;nofollow&quot;&gt;1e10&lt;/a&gt;"},{"from_user_id_str":"1594784","profile_image_url":"http://a2.twimg.com/profile_images/378515773/square-profile_normal.jpg","created_at":"Wed, 26 Jan 2011 04:01:29 +0000","from_user":"ojrac","id_str":"30113067333324800","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @tomheon: @ojrac @chewedwire Don't worry, learning Haskell will not give you any clear idea what monad means.","id":30113067333324800,"from_user_id":1594784,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"207589736","profile_image_url":"http://a3.twimg.com/profile_images/1225527428/headshot_1_normal.jpg","created_at":"Wed, 26 Jan 2011 04:00:13 +0000","from_user":"ashleevelazq101","id_str":"30112747555397632","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Federal investigation finds safety violations at The Acadia Hospital: By Meg Haskell, BDN Staff The investigatio... http://bit.ly/dONnpn","id":30112747555397632,"from_user_id":207589736,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"17671137","profile_image_url":"http://a2.twimg.com/profile_images/290264834/Haskell-logo-outer-glow_normal.png","created_at":"Wed, 26 Jan 2011 03:58:00 +0000","from_user":"Hackage","id_str":"30112192346984448","metadata":{"result_type":"recent"},"to_user_id":null,"text":"streams 0.4, added by EdwardKmett: Various Haskell 2010 stream comonads http://bit.ly/idBkPe","id":30112192346984448,"from_user_id":17671137,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"17489366","profile_image_url":"http://a3.twimg.com/sticky/default_profile_images/default_profile_2_normal.png","created_at":"Wed, 26 Jan 2011 03:58:00 +0000","from_user":"aapnoot","id_str":"30112191881420800","metadata":{"result_type":"recent"},"to_user_id":null,"text":"streams 0.4, added by EdwardKmett: Various Haskell 2010 stream comonads http://bit.ly/idBkPe","id":30112191881420800,"from_user_id":17489366,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"8530482","profile_image_url":"http://a2.twimg.com/profile_images/137867266/n608671563_7396_normal.jpg","created_at":"Wed, 26 Jan 2011 03:50:12 +0000","from_user":"jeffmclamb","id_str":"30110229207187456","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Angel - daemon to run and monitor processes like daemontools or god, written in Haskell http://ff.im/-wNyLk","id":30110229207187456,"from_user_id":8530482,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://friendfeed.com&quot; rel=&quot;nofollow&quot;&gt;FriendFeed&lt;/a&gt;"},{"from_user_id_str":"177539201","profile_image_url":"http://a0.twimg.com/profile_images/1178368800/img_normal.jpeg","created_at":"Wed, 26 Jan 2011 03:46:01 +0000","from_user":"tomheon","id_str":"30109174645919744","metadata":{"result_type":"recent"},"to_user_id":1594784,"text":"@ojrac @chewedwire Don't worry, learning Haskell will not give you any clear idea what monad means.","id":30109174645919744,"from_user_id":177539201,"to_user":"ojrac","geo":null,"iso_language_code":"en","to_user_id_str":"1594784","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"1594784","profile_image_url":"http://a2.twimg.com/profile_images/378515773/square-profile_normal.jpg","created_at":"Wed, 26 Jan 2011 03:44:34 +0000","from_user":"ojrac","id_str":"30108808684503040","metadata":{"result_type":"recent"},"to_user_id":128028225,"text":"@chewedwire @tomheon Why are you making me curious about Haskell? I LIKE not knowing what monad means!!","id":30108808684503040,"from_user_id":1594784,"to_user":"chewedwire","geo":null,"iso_language_code":"en","to_user_id_str":"128028225","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"23750094","profile_image_url":"http://a0.twimg.com/profile_images/951373780/Moeinthecar_normal.jpg","created_at":"Wed, 26 Jan 2011 03:41:54 +0000","from_user":"shokalshab","id_str":"30108140443795456","metadata":{"result_type":"recent"},"to_user_id":null,"text":"I'm at Magnitude Cheer @ Gymnastics Olympica USA (7735 Haskell Ave., btw Saticoy &amp; Strathern, Van Nuys) http://4sq.com/gmXfaL","id":30108140443795456,"from_user_id":23750094,"geo":null,"iso_language_code":"en","place":{"id":"4e4a2a2f86cb2946","type_":"poi","full_name":"Gymnastics Olympica USA, Van Nuys"},"to_user_id_str":null,"source":"&lt;a href=&quot;http://foursquare.com&quot; rel=&quot;nofollow&quot;&gt;foursquare&lt;/a&gt;"},{"from_user_id_str":"8135112","profile_image_url":"http://a0.twimg.com/profile_images/1165240350/LIMITED_normal.jpg","created_at":"Wed, 26 Jan 2011 03:41:35 +0000","from_user":"Claricei","id_str":"30108059208515584","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @kristenmchugh22: @KeithOlbermann Ryan looks like Jughead w/o the hat. And sounds as mealy-mouthed as Eddie Haskell.","id":30108059208515584,"from_user_id":8135112,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"128028225","profile_image_url":"http://a1.twimg.com/profile_images/1224994593/Coding_Drunk_normal.jpg","created_at":"Wed, 26 Jan 2011 03:40:02 +0000","from_user":"chewedwire","id_str":"30107670367182848","metadata":{"result_type":"recent"},"to_user_id":177539201,"text":"@tomheon Cool, I'll take a look. I feel like I should mention this: http://bit.ly/hVstDM","id":30107670367182848,"from_user_id":128028225,"to_user":"tomheon","geo":null,"iso_language_code":"en","to_user_id_str":"177539201","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"8679778","profile_image_url":"http://a3.twimg.com/profile_images/1195318056/me_nyc_12_18_10_icon_normal.jpg","created_at":"Wed, 26 Jan 2011 03:38:42 +0000","from_user":"kristenmchugh22","id_str":"30107332381777920","metadata":{"result_type":"recent"},"to_user_id":756269,"text":"@KeithOlbermann Ryan looks like Jughead w/o the hat. And sounds as mealy-mouthed as Eddie Haskell.","id":30107332381777920,"from_user_id":8679778,"to_user":"KeithOlbermann","geo":null,"iso_language_code":"en","to_user_id_str":"756269","source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"103316559","profile_image_url":"http://a1.twimg.com/profile_images/1179458751/bc3beab4-d59d-4e78-b13b-50747986cfa2_normal.png","created_at":"Wed, 26 Jan 2011 03:36:15 +0000","from_user":"cityslikr","id_str":"30106719153557504","metadata":{"result_type":"recent"},"to_user_id":null,"text":"&quot;Social safety net into a hammock.&quot; So says Eddie Haskell with the GOP response. #SOTU","id":30106719153557504,"from_user_id":103316559,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://blackberry.com/twitter&quot; rel=&quot;nofollow&quot;&gt;Twitter for BlackBerry\u00ae&lt;/a&gt;"},{"from_user_id_str":"169063143","profile_image_url":"http://a0.twimg.com/profile_images/1160506212/9697_1_normal.gif","created_at":"Wed, 26 Jan 2011 03:31:52 +0000","from_user":"wrkforce_safety","id_str":"30105614919143424","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Federal investigation finds safety violations at The Acadia Hospital: By Meg Haskell, BDN Staff The investigatio... http://bit.ly/gA60C1","id":30105614919143424,"from_user_id":169063143,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"177539201","profile_image_url":"http://a0.twimg.com/profile_images/1178368800/img_normal.jpeg","created_at":"Wed, 26 Jan 2011 03:29:40 +0000","from_user":"tomheon","id_str":"30105060960632832","metadata":{"result_type":"recent"},"to_user_id":128028225,"text":"@chewedwire Great book on Haskell: http://oreilly.com/catalog/9780596514983","id":30105060960632832,"from_user_id":177539201,"to_user":"chewedwire","geo":null,"iso_language_code":"en","to_user_id_str":"128028225","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"782692","profile_image_url":"http://a0.twimg.com/profile_images/1031304589/Profile.2007.1_normal.jpg","created_at":"Wed, 26 Jan 2011 03:29:19 +0000","from_user":"turnageb","id_str":"30104974591533057","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @ovillalon: Paul Ryan solves the mystery of whatever happened to Eddie Haskell. #sotu","id":30104974591533057,"from_user_id":782692,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"128028225","profile_image_url":"http://a1.twimg.com/profile_images/1224994593/Coding_Drunk_normal.jpg","created_at":"Wed, 26 Jan 2011 03:28:04 +0000","from_user":"chewedwire","id_str":"30104657267265536","metadata":{"result_type":"recent"},"to_user_id":177539201,"text":"@tomheon I always loved the pattern matching in SML and it looks like Haskell is MUCH better at it. I'm messing around now at tryhaskell.org","id":30104657267265536,"from_user_id":128028225,"to_user":"tomheon","geo":null,"iso_language_code":"en","to_user_id_str":"177539201","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"12834082","profile_image_url":"http://a3.twimg.com/profile_images/1083036140/mugshot_normal.png","created_at":"Wed, 26 Jan 2011 03:28:01 +0000","from_user":"ovillalon","id_str":"30104647213518848","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Paul Ryan solves the mystery of whatever happened to Eddie Haskell. #sotu","id":30104647213518848,"from_user_id":12834082,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"13540930","profile_image_url":"http://a1.twimg.com/profile_images/1205312219/23467_350321383101_821143101_5114147_3010041_n_normal.jpg","created_at":"Wed, 26 Jan 2011 03:26:02 +0000","from_user":"goodfox","id_str":"30104146455560192","metadata":{"result_type":"recent"},"to_user_id":10226179,"text":"@billykeene22 Bordeaux is one of my heroes. I was so excited when he accepted the invitation to campus. He's been a great friend to Haskell.","id":30104146455560192,"from_user_id":13540930,"to_user":"billykeene22","geo":null,"iso_language_code":"en","to_user_id_str":"10226179","source":"&lt;a href=&quot;http://www.ubertwitter.com/bb/download.php&quot; rel=&quot;nofollow&quot;&gt;\u00dcberTwitter&lt;/a&gt;"},{"from_user_id_str":"18616016","profile_image_url":"http://a2.twimg.com/profile_images/1173522726/214397343_normal.jpg","created_at":"Wed, 26 Jan 2011 03:25:18 +0000","from_user":"josej30","id_str":"30103962313031681","metadata":{"result_type":"recent"},"to_user_id":14870909,"text":"@cris7ian Ahh bueno multiparadigma ya es respetable :) Empezar\u00e9 a explotar la parte funcional de los lenguajes ahora #Haskell","id":30103962313031681,"from_user_id":18616016,"to_user":"Cris7ian","geo":null,"iso_language_code":"es","to_user_id_str":"14870909","source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"14870909","profile_image_url":"http://a2.twimg.com/profile_images/1176930429/oso_yo_normal.png","created_at":"Wed, 26 Jan 2011 03:23:43 +0000","from_user":"Cris7ian","id_str":"30103562360983553","metadata":{"result_type":"recent"},"to_user_id":18616016,"text":"@josej30 hahaha no, es multiparadigma y es bastante lazy. Nothing like haskell, pero s\u00ed, el de Flash","id":30103562360983553,"from_user_id":14870909,"to_user":"josej30","geo":null,"iso_language_code":"es","to_user_id_str":"18616016","source":"&lt;a href=&quot;http://www.echofon.com/&quot; rel=&quot;nofollow&quot;&gt;Echofon&lt;/a&gt;"},{"from_user_id_str":"2421643","profile_image_url":"http://a0.twimg.com/profile_images/1190361665/ernestgrumbles-17_normal.jpg","created_at":"Wed, 26 Jan 2011 03:20:24 +0000","from_user":"ernestgrumbles","id_str":"30102730756333568","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Wow... WolframAlpha did not know who Eddie Haskell is.  Guess I'll never use that &quot;knowledge engine&quot; again.","id":30102730756333568,"from_user_id":2421643,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"128028225","profile_image_url":"http://a1.twimg.com/profile_images/1224994593/Coding_Drunk_normal.jpg","created_at":"Wed, 26 Jan 2011 03:14:21 +0000","from_user":"chewedwire","id_str":"30101204428132352","metadata":{"result_type":"recent"},"to_user_id":177539201,"text":"@tomheon How is Haskell better/different from CL or Scheme? I honestly don't know, although I'm becoming more curious.","id":30101204428132352,"from_user_id":128028225,"to_user":"tomheon","geo":null,"iso_language_code":"en","to_user_id_str":"177539201","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"13540930","profile_image_url":"http://a1.twimg.com/profile_images/1205312219/23467_350321383101_821143101_5114147_3010041_n_normal.jpg","created_at":"Wed, 26 Jan 2011 03:06:54 +0000","from_user":"goodfox","id_str":"30099329809129473","metadata":{"result_type":"recent"},"to_user_id":null,"text":"A day of vision &amp; speeches. #SOTU now. And a wonderful Haskell Convocation address earlier today by Sinte Gleske President Lionel Bordeaux.","id":30099329809129473,"from_user_id":13540930,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.ubertwitter.com/bb/download.php&quot; rel=&quot;nofollow&quot;&gt;\u00dcberTwitter&lt;/a&gt;"},{"from_user_id_str":"119185220","profile_image_url":"http://a0.twimg.com/profile_images/1089027228/dfg_normal.jpg","created_at":"Wed, 26 Jan 2011 03:03:38 +0000","from_user":"LaLiciouz_03","id_str":"30098510623805440","metadata":{"result_type":"recent"},"to_user_id":null,"text":"The Game with the girl room 330 Haskell follow us...","id":30098510623805440,"from_user_id":119185220,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"18616016","profile_image_url":"http://a2.twimg.com/profile_images/1173522726/214397343_normal.jpg","created_at":"Wed, 26 Jan 2011 02:57:25 +0000","from_user":"josej30","id_str":"30096946144215040","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Voy a extra\u00f1ar Haskell cuando regrese al mundo imperativo. Hay alg\u00fan lenguaje imperativo que tenga este poder funcional? #ci3661","id":30096946144215040,"from_user_id":18616016,"geo":null,"iso_language_code":"es","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"17671137","profile_image_url":"http://a2.twimg.com/profile_images/290264834/Haskell-logo-outer-glow_normal.png","created_at":"Wed, 26 Jan 2011 02:55:32 +0000","from_user":"Hackage","id_str":"30096471814574080","metadata":{"result_type":"recent"},"to_user_id":null,"text":"comonad-transformers 0.9.0, added by EdwardKmett: Haskell 98 comonad transformers http://bit.ly/h6xIsf","id":30096471814574080,"from_user_id":17671137,"geo":null,"iso_language_code":"no","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitterfeed.com&quot; rel=&quot;nofollow&quot;&gt;twitterfeed&lt;/a&gt;"},{"from_user_id_str":"177539201","profile_image_url":"http://a0.twimg.com/profile_images/1178368800/img_normal.jpeg","created_at":"Wed, 26 Jan 2011 02:48:12 +0000","from_user":"tomheon","id_str":"30094626920603649","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Every time I look at Haskell I love it more.","id":30094626920603649,"from_user_id":177539201,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"60792568","profile_image_url":"http://a0.twimg.com/profile_images/1203647517/glenda-flash_normal.jpg","created_at":"Wed, 26 Jan 2011 02:40:42 +0000","from_user":"r_takaishi","id_str":"30092735935422464","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Haskell\u3088\u308aD\u8a00\u8a9e\u304c\u4e0a\u3068\u306f\u601d\u308f\u306a\u304b\u3063\u305f\uff0e http://www.tiobe.com/index.php/content/paperinfo/tpci/index.html","id":30092735935422464,"from_user_id":60792568,"geo":null,"iso_language_code":"ja","to_user_id_str":null,"source":"&lt;a href=&quot;http://twmode.sf.net/&quot; rel=&quot;nofollow&quot;&gt;twmode&lt;/a&gt;"},{"from_user_id_str":"160145510","profile_image_url":"http://a0.twimg.com/profile_images/1218108166/going_galt_normal.jpg","created_at":"Wed, 26 Jan 2011 02:39:31 +0000","from_user":"wtp1787","id_str":"30092439653974018","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @KLSouth: Eddie Haskell Goes to Washington...  &quot;You look really nice tonight, Ms Cleaver&quot;","id":30092439653974018,"from_user_id":160145510,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"96616016","profile_image_url":"http://a1.twimg.com/profile_images/1196978169/Picture0002_normal.jpg","created_at":"Wed, 26 Jan 2011 02:39:20 +0000","from_user":"MelissaRNMBA","id_str":"30092392203816960","metadata":{"result_type":"recent"},"to_user_id":14862975,"text":"@KLSouth At least Eddie Haskell was entertaining.","id":30092392203816960,"from_user_id":96616016,"to_user":"KLSouth","geo":null,"iso_language_code":"en","to_user_id_str":"14862975","source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"14862975","profile_image_url":"http://a0.twimg.com/profile_images/421596393/kls_4_normal.JPG","created_at":"Wed, 26 Jan 2011 02:38:29 +0000","from_user":"KLSouth","id_str":"30092178327871489","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Eddie Haskell Goes to Washington...  &quot;You look really nice tonight, Ms Cleaver&quot;","id":30092178327871489,"from_user_id":14862975,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.tweetdeck.com&quot; rel=&quot;nofollow&quot;&gt;TweetDeck&lt;/a&gt;"},{"from_user_id_str":"144546280","profile_image_url":"http://a1.twimg.com/a/1295051201/images/default_profile_1_normal.png","created_at":"Wed, 26 Jan 2011 02:36:17 +0000","from_user":"listwarenet","id_str":"30091626869161984","metadata":{"result_type":"recent"},"to_user_id":null,"text":"http://www.listware.net/201101/haskell-beginners/84641-haskell-beginners-wildcards-in-expressions.html Haskell-beginners -  Wild","id":30091626869161984,"from_user_id":144546280,"geo":null,"iso_language_code":"no","to_user_id_str":null,"source":"&lt;a href=&quot;http://1e10.org/cloud/&quot; rel=&quot;nofollow&quot;&gt;1e10&lt;/a&gt;"},{"from_user_id_str":"24538048","profile_image_url":"http://a2.twimg.com/profile_images/1117267605/rope_normal.jpg","created_at":"Wed, 26 Jan 2011 02:23:14 +0000","from_user":"dbph","id_str":"30088341030440960","metadata":{"result_type":"recent"},"to_user_id":null,"text":"RT @dnene: Skilled Calisthenics. Haskell code that outputs python which spits ruby which emits the haskell source. http://j.mp/YlQUL via @mfeathers","id":30088341030440960,"from_user_id":24538048,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"},{"from_user_id_str":"158951567","profile_image_url":"http://a3.twimg.com/profile_images/962721419/Logo_3_normal.jpg","created_at":"Wed, 26 Jan 2011 01:58:31 +0000","from_user":"YubaVetTech","id_str":"30082124207886336","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Dr. Haskell has just been awarded the prestigious Hayward Award for \u2018Excellence in Education\u2019. This award honors... http://fb.me/QgQCxd74","id":30082124207886336,"from_user_id":158951567,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.facebook.com/twitter&quot; rel=&quot;nofollow&quot;&gt;Facebook&lt;/a&gt;"},{"from_user_id_str":"158951567","profile_image_url":"http://a3.twimg.com/profile_images/962721419/Logo_3_normal.jpg","created_at":"Wed, 26 Jan 2011 01:56:04 +0000","from_user":"YubaVetTech","id_str":"30081505476743168","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Dr. Haskell has just been awarded the prestigous Haward Award for Excellence in Education. This award honors... http://fb.me/PC9mYmCR","id":30081505476743168,"from_user_id":158951567,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://www.facebook.com/twitter&quot; rel=&quot;nofollow&quot;&gt;Facebook&lt;/a&gt;"},{"from_user_id_str":"2331498","profile_image_url":"http://a3.twimg.com/profile_images/494632120/me_normal.jpg","created_at":"Wed, 26 Jan 2011 01:43:50 +0000","from_user":"Verus","id_str":"30078427155406848","metadata":{"result_type":"recent"},"to_user_id":79273052,"text":"@kami_joe \u3044\u3084\uff0c\u8ab2\u984c\u306f\u89e3\u6c7a\u5bfe\u8c61(\u30d1\u30ba\u30eb\u3068\u304b)\u3092\u4e0e\u3048\u3089\u308c\u3066\uff0c\u554f\u984c\u5b9a\u7fa9\u3068\u30d7\u30ed\u30b0\u30e9\u30df\u30f3\u30b0(\u6307\u5b9a\u8a00\u8a9e\u306fC++\u3082\u3057\u304f\u306fJava)\u3068\u3044\u3046\u3044\u308f\u3070\u666e\u901a\u306a\u8ab2\u984c\u3067\u306f\u3042\u308b\u3093\u3060\u3051\u3069\uff0e\u95a2\u6570\u578b\u8a00\u8a9e\u306fHaskell\u306e\u6388\u696d\u304c\u307e\u305f\u5225\u306b\u3042\u308b\u306e\uff0e","id":30078427155406848,"from_user_id":2331498,"to_user":"kami_joe","geo":null,"iso_language_code":"ja","to_user_id_str":"79273052","source":"&lt;a href=&quot;http://itunes.apple.com/us/app/twitter/id409789998?mt=12&quot; rel=&quot;nofollow&quot;&gt;Twitter for Mac&lt;/a&gt;"},{"from_user_id_str":"79151233","profile_image_url":"http://a2.twimg.com/a/1295051201/images/default_profile_2_normal.png","created_at":"Wed, 26 Jan 2011 01:40:37 +0000","from_user":"cz_newdrafts","id_str":"30077617361125376","metadata":{"result_type":"recent"},"to_user_id":null,"text":"Haskell programming language http://bit.ly/gpPAwB","id":30077617361125376,"from_user_id":79151233,"geo":null,"iso_language_code":"no","to_user_id_str":null,"source":"&lt;a href=&quot;http://tommorris.org/&quot; rel=&quot;nofollow&quot;&gt;tommorris' hacksample&lt;/a&gt;"},{"from_user_id_str":"2331498","profile_image_url":"http://a3.twimg.com/profile_images/494632120/me_normal.jpg","created_at":"Wed, 26 Jan 2011 01:02:57 +0000","from_user":"Verus","id_str":"30068139416879104","metadata":{"result_type":"recent"},"to_user_id":9252720,"text":"@shukukei Java\u3068C\u306f\u3042\u308b\u7a0b\u5ea6\u66f8\u3051\u3066\u3042\u305f\u308a\u307e\u3048\u306a\u3068\u3053\u308d\u304c\u3042\u308b\u304b\u3089\u306a\u30fc\uff0ePython\u306f\u500b\u4eba\u7684\u306b\u611f\u899a\u304c\u5408\u308f\u306a\u3044\uff0e\u611f\u899a\u306a\u306e\u3067\uff0c\u3082\u3046\u3069\u3046\u3057\u3088\u3046\u3082\u306a\u3044\uff57 \u3044\u307e\u306fScala\u3068Haskell\u3092\u3082\u3063\u3068\u6975\u3081\u305f\u3044\u3068\u3053\u308d\uff0e","id":30068139416879104,"from_user_id":2331498,"to_user":"shukukei","geo":null,"iso_language_code":"ja","to_user_id_str":"9252720","source":"&lt;a href=&quot;http://itunes.apple.com/us/app/twitter/id409789998?mt=12&quot; rel=&quot;nofollow&quot;&gt;Twitter for Mac&lt;/a&gt;"},{"from_user_id_str":"2781460","profile_image_url":"http://a0.twimg.com/profile_images/82526625/.joeyicon_normal.jpg","created_at":"Wed, 26 Jan 2011 01:02:35 +0000","from_user":"joeyhess","id_str":"30068046538219520","metadata":{"result_type":"recent"},"to_user_id":null,"text":"just figured out that I can use parameterized types to remove a dependency loop in git-annex's type definitions. whee #haskell","id":30068046538219520,"from_user_id":2781460,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://identi.ca&quot; rel=&quot;nofollow&quot;&gt;identica&lt;/a&gt;"},{"from_user_id_str":"144546280","profile_image_url":"http://a1.twimg.com/a/1295051201/images/default_profile_1_normal.png","created_at":"Wed, 26 Jan 2011 00:54:22 +0000","from_user":"listwarenet","id_str":"30065977920061440","metadata":{"result_type":"recent"},"to_user_id":null,"text":"http://www.listware.net/201101/haskell-beginners/84466-haskell-beginners-bytestring-question.html Haskell-beginners -  Bytestrin","id":30065977920061440,"from_user_id":144546280,"geo":null,"iso_language_code":"no","to_user_id_str":null,"source":"&lt;a href=&quot;http://1e10.org/cloud/&quot; rel=&quot;nofollow&quot;&gt;1e10&lt;/a&gt;"},{"from_user_id_str":"1291845","profile_image_url":"http://a0.twimg.com/profile_images/1225743404/ThinOxygen-small-opaque-solidarity_normal.png","created_at":"Wed, 26 Jan 2011 00:52:07 +0000","from_user":"_aaron_","id_str":"30065412242669568","metadata":{"result_type":"recent"},"to_user_id":null,"text":"wanted: librly licensed high level native compiled lang with min runtime (otherwise cobra/mono would be perfect) for win. lua? haskell? ooc?","id":30065412242669568,"from_user_id":1291845,"geo":null,"iso_language_code":"en","to_user_id_str":null,"source":"&lt;a href=&quot;http://twitter.com/&quot;&gt;web&lt;/a&gt;"}],"max_id":30120402839666689,"since_id":0,"refresh_url":"?since_id=30120402839666689&q=haskell","next_page":"?page=2&max_id=30120402839666689&rpp=50&q=haskell","results_per_page":50,"page":1,"completed_in":0.291696,"since_id_str":"0","max_id_str":"30120402839666689","query":"haskell"}
+ tools/check_release.py view
@@ -0,0 +1,126 @@+#!/usr/bin/env python3+"""Build the actual Hackage source archive, independently of the checkout.++No upload or database mutation occurs. --build adds archive-only compilation,+installed-data lookup, existing unit/property tests, example compilation and+Hackage Haddock generation. Keep the output directory as reviewable evidence.+"""+import argparse+import hashlib+import json+import os+from pathlib import Path+import re+import shutil+import subprocess+import tarfile++ROOT = Path(__file__).resolve().parents[1]+REQUIRED = ('mariadb-haskell.cabal', 'README.md', 'ChangeLog.md', 'LICENSE', 'LICENSE-MPL-2.0',+            'docs/USER_GUIDE.md', 'docs/BENCHMARKS.md', 'docs/MIGRATION.md',+            'docs/RELEASING.md', 'docs/CA_BUNDLE.md', 'ca-bundle.json',+            'examples/Quickstart.hs', 'examples/GuideExamples.hs',+            'cbits/gssapi.c', 'mozillaCAStore.pem', 'tools/update_ca_bundle.py')+++def command(args, cwd, log, env=None):+    with log.open('w') as output:+        subprocess.run(list(map(str, args)), cwd=cwd, stdout=output,+                       stderr=subprocess.STDOUT, check=True, env=env)+++def unpack(archive, destination, identity):+    """Accept only the expected single package tree of regular source files."""+    names = set()+    with tarfile.open(archive) as source:+        for member in source.getmembers():+            path = Path(member.name)+            if not path.parts or path.is_absolute() or '..' in path.parts or path.parts[0] != identity:+                raise ValueError('unexpected source archive path: ' + member.name)+            if not (member.isdir() or member.isfile()):+                raise ValueError('unexpected special file in source archive: ' + member.name)+            relative = path.relative_to(identity)+            if (relative.name.startswith('cabal.project') or '.local' in relative.parts+                    or '.git' in relative.parts or 'dist-newstyle' in relative.parts):+                raise ValueError('developer/private file leaked into source archive: ' + str(relative))+            names.add(str(relative))+            target = destination / path+            if member.isdir():+                target.mkdir(parents=True, exist_ok=True)+            else:+                target.parent.mkdir(parents=True, exist_ok=True)+                with source.extractfile(member) as data, target.open('wb') as output:+                    shutil.copyfileobj(data, output)+    missing = set(REQUIRED) - names+    if missing:+        raise ValueError('required source-package files absent: ' + ', '.join(sorted(missing)))+    return destination / identity+++def main():+    parser = argparse.ArgumentParser(description=__doc__)+    parser.add_argument('--output', required=True, type=Path, help='new evidence directory')+    parser.add_argument('--build', action='store_true', help='compile/test/document the unpacked archive')+    args = parser.parse_args()+    output = args.output.resolve()+    output.mkdir(parents=True, exist_ok=False)+    manifest = (ROOT / 'mariadb-haskell.cabal').read_text()+    version = re.search(r'^version:\s*(\S+)', manifest, re.M).group(1)+    identity = 'mariadb-haskell-' + version+    artifacts = output / 'artifacts'+    artifacts.mkdir()+    status = {'package': identity, 'complete': False, 'checks': []}++    def run(label, argv, cwd=ROOT, env=None):+        command(argv, cwd, output / (label + '.log'), env=env)+        status['checks'].append(label)+        print('PASS ' + label, flush=True)++    try:+        run('cabal-check', ['cabal', 'check'])+        run('ca-bundle', ['python3', 'tools/update_ca_bundle.py', '--check'])+        run('sdist', ['cabal', 'sdist', '--output-directory=' + str(artifacts)])+        archive = artifacts / (identity + '.tar.gz')+        package = unpack(archive, output / 'source', identity)+        run('archive-ca', ['python3', 'tools/update_ca_bundle.py', '--check'], package)+        if args.build:+            target = 'lib:mariadb-haskell'+            run('archive-build', ['cabal', 'build', target, 'test:test', '--enable-tests', '-O2'], package)+            plan = json.loads((package / 'dist-newstyle/cache/plan.json').read_text())+            local = [entry for entry in plan['install-plan'] if entry['pkg-name'] == 'mariadb-haskell']+            if not local or any(entry.get('flags', {}).get('validation') is not False for entry in local):+                raise ValueError('source archive unexpectedly enables the developer validation component')+            setups = list((package / 'dist-newstyle/build').glob('*/ghc-*/mariadb-haskell-*/opt/setup-config'))+            if len(setups) != 1:+                raise ValueError('expected exactly one archive library installation configuration')+            run('archive-install-data', ['cabal', 'v1-copy', target, '--builddir=' + str(setups[0].parent)], package)+            # Compile inside Cabal, then run outside both Cabal and the package+            # directory. A local data override must not mask a broken install.+            probe = output / 'InstalledCA.hs'+            probe.write_text('module Main where\nimport Data.TLSSetting\n'+                             'main :: IO ()\nmain = makeClientParams MozillaCAStore >> putStrLn "Installed CA store loaded"\n')+            run('installed-ca-build', ['cabal', 'exec', '--', 'ghc', '-threaded', '-package',+                                      'mariadb-haskell', probe, '-outputdir', output / 'ca-objects',+                                      '-o', output / 'installed-ca'], package)+            clean = {key: value for key, value in os.environ.items() if key != 'mariadb_haskell_datadir'}+            run('installed-ca', [output / 'installed-ca'], output, env=clean)+            run('archive-unit', ['cabal', 'test', 'test', '--enable-tests', '-O2',+                                '--test-options=--quickcheck-replay=730208 --quickcheck-tests=100'], package)+            run('guide-examples', ['cabal', 'exec', '--', 'ghc', '-fno-code', '-Wall',+                                  '-package', 'mariadb-haskell', 'examples/GuideExamples.hs'], package)+            run('quickstart', ['cabal', 'exec', '--', 'ghc', '-threaded', '-Wall',+                              '-package', 'mariadb-haskell', 'examples/Quickstart.hs',+                              '-outputdir', output / 'objects', '-o', output / 'quickstart'], package)+            run('haddock', ['cabal', 'haddock', target, '--haddock-for-hackage', '-O2'], package)+            shutil.copy2(package / 'dist-newstyle' / (identity + '-docs.tar.gz'), artifacts)+        status['artifacts'] = {p.name: hashlib.sha256(p.read_bytes()).hexdigest()+                               for p in sorted(artifacts.glob('*.tar.gz'))}+        (artifacts / 'SHA256SUMS').write_text(''.join(digest + '  ' + name + '\n'+                                                   for name, digest in status['artifacts'].items()))+        status['complete'] = True+    finally:+        (output / 'checks.json').write_text(json.dumps(status, indent=2) + '\n')+++if __name__ == '__main__':+    main()
+ tools/update_ca_bundle.py view
@@ -0,0 +1,82 @@+#!/usr/bin/env python3+"""Verify the installed CA snapshot or explicitly update a pinned curl extract.++Updates require a dated release and its independently reviewed SHA-256. The+published checksum must also agree. Runtime connections never fetch trust roots;+the reviewed file, notices and provenance ship together in the source package.+"""+import argparse+import datetime+import hashlib+import json+from pathlib import Path+import re+import ssl+import urllib.request++ROOT = Path(__file__).resolve().parents[1]+BUNDLE = ROOT / 'mozillaCAStore.pem'+MANIFEST = ROOT / 'ca-bundle.json'+++def validate(data, expected):+    """Check exact identity and every complete PEM block before installation."""+    digest = hashlib.sha256(data).hexdigest()+    if digest != expected:+        raise ValueError('CA bundle SHA-256 does not match the reviewed checksum')+    # PEM payloads are ASCII, but Mozilla's human-readable CA names are UTF-8.+    text = data.decode('utf-8')+    certificates = re.findall(r'-----BEGIN CERTIFICATE-----\s.*?-----END CERTIFICATE-----', text, re.S)+    if not certificates or len(certificates) != text.count('-----BEGIN CERTIFICATE-----'):+        raise ValueError('empty or incomplete CA certificate bundle')+    for certificate in certificates:+        ssl.PEM_cert_to_DER_cert(certificate)+    return len(certificates)+++def check():+    metadata = json.loads(MANIFEST.read_text())+    count = validate(BUNDLE.read_bytes(), metadata['sha256'])+    if count != metadata['certificate_count']:+        raise ValueError('CA certificate count disagrees with the provenance manifest')+    if not (ROOT / 'LICENSE-MPL-2.0').is_file():+        raise ValueError('bundled CA license is missing')+    print(f"Verified Mozilla extract {metadata['version']}: {count} certificates, {metadata['sha256']}")+++def download(url):+    with urllib.request.urlopen(url, timeout=30) as response:+        return response.read(2 * 1024 * 1024)+++def main():+    parser = argparse.ArgumentParser(description=__doc__)+    parser.add_argument('--check', action='store_true')+    parser.add_argument('--version', help='dated curl extract, YYYY-MM-DD')+    parser.add_argument('--sha256', help='reviewed checksum from the curl CA extract page')+    args = parser.parse_args()+    if args.check:+        if args.version or args.sha256:+            parser.error('--check cannot update a bundle')+        check()+        return+    if not args.version or not args.sha256 or not re.fullmatch('[0-9a-f]{64}', args.sha256):+        parser.error('an update requires --version YYYY-MM-DD and --sha256 HEX')+    datetime.date.fromisoformat(args.version)+    url = f'https://curl.se/ca/cacert-{args.version}.pem'+    data = download(url)+    published = download(url + '.sha256').decode('ascii').split()[0]+    if published != args.sha256:+        raise ValueError('published checksum differs from the reviewed checksum')+    count = validate(data, args.sha256)+    metadata = {'version': args.version, 'source': url, 'checksum_source': url + '.sha256',+                'sha256': args.sha256, 'certificate_count': count, 'license': 'MPL-2.0',+                'source_information': 'https://curl.se/docs/caextract.html',+                'retrieved_at': datetime.datetime.now(datetime.timezone.utc).isoformat()}+    BUNDLE.write_bytes(data)+    MANIFEST.write_text(json.dumps(metadata, indent=2) + '\n')+    check()+++if __name__ == '__main__':+    main()
+ validation/COVERAGE.md view
@@ -0,0 +1,230 @@+# MariaDB validation coverage++The initial baseline targets the installed MariaDB **12.3.3 LTS** server and GHC **9.10.3** on macOS arm64, using unchanged driver source from upstream commit `92a94012bc553a672c73d409dad1d17e9db1e53f`. Current implementation results describe the fingerprinted source separately. The suite checks the driver surfaces below; assertions describe correct behavior rather than adapting expected values to current defects.++## Correctness coverage++| Surface | Checks |+| --- | --- |+| Protocol and framing | Length integer boundaries, packet round trips, every split within a small packet, coalesced packets, short headers/bodies, continuation sequence errors and wrap, EOF/large-row distinction, unsigned OK IDs, column metadata encoding, binary authentication challenges, unknown plugins, capability markers, empty packet handling, numeric overflow/trailing input, synthetic reset state and NULL bitmaps |+| Connections and TLS | Native TCP, Unix sockets, verified TLS, missing/invalid credentials/schema, hostname and CA rejection, IPv4/IPv6 localhost fallback, full-Unicode defaults and installed CA-bundle lookup |+| Authentication | Native password, OS-mapped Unix socket, ed25519/parsec/SHA-2 over TCP/TLS/socket, X.509-required users, valid/expired client certificates, certificate-request callback selection and helper behavior. CLI controls establish that the configured accounts work independently of the driver. |+| Scalar values | Signed/unsigned integer widths and extrema, MariaDB MEDIUMINT, DECIMAL precision 65 with scale 30/38, exact float/double values, BIT 1/8/16/64, YEAR 0/1901/2155, valid date extrema/leap day, fractional DATETIME/TIMESTAMP/TIME, TIMESTAMP beyond 2038, TIME ±838 hours, NULL, ENUM/SET, empty strings/bytes, all 256 byte values, NUL/control characters, multilingual/combining/supplementary Unicode |+| Text and binary parity | Each of 47 scalar fixtures is inserted using binary binding and fetched independently through text and prepared binary results. NULL row bitmaps cross 1/2/7/8/9/15/16/17/63/64/65 columns. |+| Native MariaDB values | JSON with supplementary Unicode, UUID, INET4/INET6, packed VECTOR values and textual conversion, GEOMETRY and coordinate functions, zero-date reinsertion, unsigned generated IDs, non-UTF-8 results |+| SQL and results | CTE/recursive CTE, window functions, JSON functions, sequence tables/objects, generated columns, CHECK/foreign-key constraints, text/prepared INSERT/DELETE RETURNING, procedure multiple results and prepared OUT parameters, Oracle mode, system-versioned history, list/vector streams, empty/non-row results, mixed multi-statements and deliberate draining |+| Parameters and prepared lifecycle | Literal/comment question marks, NO_BACKSLASH_ESCAPES through both APIs, SQL-looking bound data, IN-list/empty-list substitution, parameter type changes, arity, metadata names/unsigned/scale/counts, reset, closed IDs and DDL reprepare |+| Session and errors | Schema switching, warning counts, TIMESTAMP versus DATETIME timezone behavior, syntax/table/duplicate errors followed by a valid query |+| Large packets | Live first-field BLOB results below/at/above 16 KiB and 16 MiB, up to 33,554,430 bytes. Prepared and text outbound parameters cross physical packet boundaries. Every live case checks content and follows with another query. Inherited tests add their original large-table/value cases. |+| Transactions | Commit, rollback, savepoints, original exception preservation after unfinished rows, nesting, read-committed visibility, lock timeout, deadlock and reuse, MariaDB DDL implicit commit |+| Failures and cancellation | Unread-result guard, reset with pending rows, timeout before metadata, server-side query kill, peer disconnect, TCP and TLS asynchronous cancellation |+| Binlog and CDC | Live insert, synthetic single/multiple/reused table maps, table-ID mismatch, damaged checksum rejection, total decoding of MariaDB event IDs 160–171. GTID resume and durable acknowledgement controls are explicit unsupported inventory entries. |++The initial harness has **313 entries**: 300 executable checks, 11 explicit missing-API inventory entries and two environment inventory entries. The inherited suites add **179 unit/property cases** and **32 integration cases**, run without editing their test code. The inherited unit suite retains one ignored external HTTPS test; the new suite exercises MariaDB TLS directly. Seeded QuickCheck checks run 100 examples per property. The runner's own eight unit checks verify regression classification and percentile calculation.++Inventory entries are not counted as passing tests. At this baseline, native bulk execution, prepared metadata caching, extended metadata/session tracking, connection reset, progress callbacks, cursor fetch, long-data upload, transport compression, GTID resume and durable CDC acknowledgement controls have no public implementation to invoke. Their absence is retained as `UNSUPPORTED`, including in the benchmark inventory where applicable.++Stage 1 adds **88 executable cases** separately from the frozen 300: real lost-COMMIT-acknowledgement/fragmenting/disconnecting proxies; actual command counts; pre-body continuation limits; malformed metadata/OK/rows; ownership, stale streams and transaction leases; bounded cleanup and new cancellation; unknown-context refresh, raw/prepared tracking replacement, selection preservation, unknown tracker items and both terminator modes; generation-safe command handoff, terminal close transitions, comment-ended batches, cancellation in transport cleanup, replication setup retirement, strict session snapshots and fractional-TIME bounds; executable-comment tracking invalidation, server-prepared lexer parity, optional batch delimiters and bounded startup facts; charset binding and strict decoding; raw dates; Word64 boundaries; injection/quote/comment/mode/batch cases. The Stage 1 run has 403 records including the same 13 inventory entries and two inherited suite summaries. Assertions and original workloads are preserved. The eight original runner-policy tests, fifteen publication tests, twenty-two acceptance/reference tests and six wrapper-failure tests run independently of MariaDB.++The separate memory experiments measure live heap at GC checkpoints across 10,000 and 1,000,000 rows, plus one retained byte after a 16 MiB result. They supplement process high-water statistics and do not change any frozen throughput benchmark. Exact current outcomes and open investigations are recorded in the live report and [implementation notes](../docs/STAGE1_IMPLEMENTATION_NOTES.md).++PAM and GSSAPI have no configured OS/realm login fixtures on this Mac. The Homebrew PAM helper could not acquire root ownership during installation, and there is no Kerberos realm/keytab. They are `ENVIRONMENT_UNAVAILABLE`. Platform-specific Windows authentication/transports require a Windows test profile; this Mac run makes no claim about them.++## Performance coverage++| Workload | Timing unit and validation | Control |+| --- | --- | --- |+| Text/prepared SELECT 1 | One query; 500 measured queries/process; checksum 1 each | C text |+| Narrow scans | 10,000 rows with two BIGINTs and one 128-byte ASCII payload; text/prepared/vector, TCP/TLS/socket | C text TCP |+| Wide scans | 10,000 rows with two BIGINTs and 30 payload columns; text/prepared | C text |+| Independent readers | 4/16 connections, each streaming the narrow dataset; connections established before timing | Haskell only |+| INSERT transactions | 1,000 rows, sequential text/prepared or semicolon batch; same TRUNCATE/BEGIN/inserts/COMMIT/count/sum work | C text and semicolon batch |+| BLOB reads | 1 MiB value per query, text/prepared; checksum equals byte length | C text |+| Decoder microbenchmarks | 100,000 text/binary rows with varying integers and a 128-byte payload; actual driver decoders and consumption | Haskell only |+| Native bulk/metadata cache | Explicit `UNSUPPORTED` until APIs exist; no substitute workload presented as equivalent | None |++There are **18 runnable Haskell workloads and six matching C controls**, plus two unsupported native-feature entries. The frozen run uses seven independently started processes per workload, each with two warm-ups. Execution order is deterministically shuffled per repetition to interleave clients. C uses MariaDB Connector/C, not a MySQL server/client compatibility target.++Raw operation samples provide p50/p95/p99 latency. Throughput is normalized by rows, BLOB bytes or decoder rows; the summary labels it ns/unit. Allocation, GC CPU and live-heap counters come from GHC RTS and are not compared to nonexistent C heap counters. Both clients verify numeric/text-byte consumption and insert count/sum. No client silently discards result payloads to inflate throughput.++## Additional environment profiles++The captured profile is one real server/OS/compiler combination. The `MB_*` connection configuration supports another prepared MariaDB instance, but its packet limit, row binlog, TLS and authentication fixtures must satisfy preflight. Capture separate baselines for a rolling release or another maintained LTS, Linux/Windows, WAN latency, compression or a different durability configuration. Compare correctness across profiles; compare performance only within matching recorded environments.++Server optimizer correctness, storage-engine internals, Galera failover, backup/recovery tools and exhaustive SQL grammar/function combinations are outside this driver's baseline. SQL pass-through is exercised by representative MariaDB features; it does not need a driver wrapper for each server function. The unsupported/environment inventory prevents those gaps being mistaken for completed validation.++## Stage 2 native connections++Stage 2 adds 30 executable cases without changing the frozen 300 or Stage 1's 88 cases. Coverage includes both capability words and exact client extension offsets; server-offered flag intersection; the independent MariaDB ed25519 signing vector; binary/terminated challenges; unknown plugins and bounded switches; PARSEC salt and work limits; malformed SHA-2 continuations and RSA inputs; real SNI with explicit opt-out; greeting deadlines and cancellation with peer-observed closure; and wrong-password rejection across three plugins and transports. All 14 original native-connection targets are required separately, and every published Stage 1 PASS is protected. There are 59 Python policy tests, including installed-data selection/failure paths and Linux/CI/reference fingerprints.++The isolated Linux profile uses real MariaDB 12.3.3, GHC 9.4.7 and Cabal 3.14.1.1. It runs all correctness checks and both inherited suites, with no cross-platform timing comparison. The macOS profile retains the original GHC 9.10.3/Cabal 3.16.1.0 benchmark comparison. See [Stage 2 evidence](../docs/STAGE2_IMPLEMENTATION.md). Windows and other server versions require additional profiles.++## Stage 3 native statements++Stage 3 adds 56 cases for complete prepared/OUT results, managed generations and+cleanup, malformed/omitted/refreshed metadata, aggregate cache budgets, owned+metadata buffers, native type attributes, bulk indicators/types/limits/encoding,+full unsigned IDs, scalar/NULL parity, 16 MiB fragmentation, transaction rollback+and observed wire command counts. The original assertions and all earlier+stage assertions are preserved. Bulk, cache and extended-metadata inventory+entries now execute real server checks. A MariaDB session counter independently+proves metadata omission. The timeout's conflicting original expectation remains+visible, with safe-retirement regressions retained. Six review regressions add+buffered statement/rollback deadlines, asynchronous cleanup cancellation,+incremental accounting through replacement/rejection/close/ID reuse, released+metadata budgets and valid bulk execution with `maxBound` resource limits.++There are 101 Python policy tests, including native record identity, checksums,+units, repetition/summary consistency, immutable-reference matching and wrapper+failure/interruption/publication behavior. Review additions cover required+baseline artifacts, recomputed publication deltas/alarms, dependency JSON+round-trips, workload versus policy changes and zero-allocation rejection.+Every complete run adds eight native+benchmark profiles to the 24 frozen runnable profiles. Native comparisons use+the same current build's alternatives and a separate immutable first native+capture; the original benchmark workloads and recorder are unchanged. Linux+also checks all eight native benchmark paths as correctness smoke runs.++## Stage 4: clean-session reuse and observations++Additive cases exercise initial snapshots, schema/variable/status and transaction+trackers, wire ordering, unknown kinds, repeated deltas, retained-byte ownership,+length/count/cumulative budgets, subscription replacement, disabled tracking and+error invalidation. Latest real envelopes remain observable after opaque SQL,+while final-state facts become explicitly unknown. Snapshot reads issue no SQL.++Reset checks cover TCP/TLS/sockets, same-session identity, rollback, server defaults,+charset and database restoration, user variables, temporary tables, advisory locks,+transaction-tracker reset, custom initialization, stale handles and raw IDs, repeated+metadata release, pending results, transaction ownership, unsupported versions,+no-database restoration limits, malformed/ERR replies, wire/restoration/buffered+deadlines, cancellation, exclusive initialization and a concurrent close.+The original session-tracking and reset capability inventory entries now run+actual server checks. CDC defects and the original timeout conflict remain visible.++Seven new profiles measure complete reset/restoration/preparation requests versus+equivalent reconnect requests on three transports, plus repeated snapshot reads.+Both alternatives check returned values and absence of preceding request state.+The first reuse reference is separate from both earlier immutable baselines; all+39 runnable profiles (24 frozen, eight native and seven reuse) have seven process+repetitions in a full run. Frozen benchmark source and enumeration stay unchanged.++Stage 4 review fixes add 16 regressions (69 Stage 4 cases total). They cover+initially absent and configured default roles; Unicode/quoted role names, latin1+clients, initializer role overrides, preservation across explicit refresh, role+restoration before database access, all transports and disabled tracking; role+restoration ERR/deadline and bounded snapshots; charset/collation alias ordering,+explicit selection and released cache budgets; and CHARACTERISTICS-to-STATE+transitions across different transactions. Role fixtures use separate reserved+accounts and leave the ordinary benchmark account's default role unchanged.++Two further role regressions (71 Stage 4 cases total) cover exact binary role+identity when a latin1 result charset collapses distinct Unicode names, temporary+UTF-8 restoration with the original narrow charset restored afterward, and binary+result settings. A third dedicated role account and two Unicode roles provide+these fixtures; provisioning remains separate from the benchmark account.++## Stage 5 CDC++83 additive cases and both converted inventory entries cover total event IDs,+known/checksummed/truncated packets, exact event lengths, owned optional TLVs,+interleaved/repeated/replaced/stale maps and budgets, bitmap boundaries and sparse+NULL/omitted fields, complete/incomplete/standalone/rollback/multidomain GTIDs,+overflow and checkpoint origin, backpressure, compressed queries/rows, fragmented+rows, foreign/backwards/idempotent ACKs, ACK cancellation, decode retirement and+ownership. Real disposable servers test actual semisync wait/release, CRC-mode+changes, rotation, DDL/minimal images and persisted-data server restart followed+by GTID resume. The managed-server fixture also tests GTID resume and cancellation.++Eight new benchmark profiles preserve all 39 previous profiles. They check actual+row values and separate pure decoding, framing/CRC, live delivery/checkpoints and+retention timing. The million-row streaming workload records post-major-GC live+heap at four checkpoints in both warmups and the measured region. Advanced XA+checkpointing and storage-engine-binlog GTID semisync are explicitly unsupported+by the transaction-aware API; new non-ignorable events fail closed.++The follow-up review adds 58 regressions (141 Stage 5 cases total): every temporal+fractional precision, negative whole/subsecond times, malformed fractions, full+GTID history after domain-origin changes, history flags/entry bounds, GTID header+and extension truncation, two-phase ALTER rejection, actual sparse-value budgets,+preserved row/value limits, `maxBound` event budgets and early continuation+rejection, and large queries with control-keyword prefixes. Disposable real servers reproduce temporal values, origin changes+across rotation and two-phase ALTER. The correctness ratchet protects all 636+published Stage 5 passes; the benchmark workloads and first captures are unchanged.++## Stage 6: streamed parameters, cursors, progress and compression++The four remaining API inventory rows now execute real feature checks. Added+coverage includes one-shot upload arity/ownership, empty non-NULL values,+byte/text charset parity, bounded chunks and cumulative budgets, source failure,+cancellation, later ordinary execution and complete procedure/OUT results.+Cursor checks cover incremental batches, final empty fetches, close/reset,+ownership, legacy/cached metadata, NULL/Unicode/DECIMAL parity and transaction+cleanup. Progress checks cover the literal MariaDB envelope, malformed fields,+callback reentry/failure/cancellation and real ALTER callbacks. Compression+checks cover raw/zlib frames, byte splits, sequence/length/expansion/trailer+errors, pre-body bounds, TCP/TLS/socket setup, 16 MiB logical continuations and+combined feature use. Transparent byte counters verify actual wire savings.+All 694 previously published passes are protected independently.++Thirteen new seven-repetition profiles measure uploads, cursor batch sizes,+narrow/wide compressed transfers and progress negotiation costs. Their separate+first reference preserves all earlier baselines. Linux smoke runs exercise every+new benchmark path without comparing its timings to Mac measurements. PAM/GSSAPI+and the retained timeout-contract conflict remain explicit.++Review-fix correctness on 9 November records **793 PASS / 1 known FAIL /+0 UNSUPPORTED / 2 unavailable** on both Mac and Linux. All 95 Stage 6 cases and+155 policy tests pass. Seven new regressions cover repeated cancellation of both+cursor brackets, compressed semisync manual/legacy on-read ACKs over three+transactions, reconnect/resume, preservation of incoming buffers across ACKs,+and rejection of whole-source buffering by the actual retained-heap guard.+The producer allocates fresh storage per chunk and the guard checks every+checkpoint. Two full captures measure all 60 profiles seven times; the second+supplied the review report. The independent first wire-feature reference and+all earlier references remain immutable. Timing investigations remain explicit.+See the [current confirmation manifest](results/stage6-confirmation.json).+++The [committed publication](results/stage6-publication.json) now supplies the+[live report](RESULTS.md), preserving the same correctness counts and all 95+Stage 6 passes. All 60 profiles have seven repetitions; timing alarms stay+explicit and no reference advances.+++## Stage 7: external authentication and timeout-contract correction++The two former environment inventory entries now run actual PAM and Kerberos+logins from the Haskell client. Forty-eight additional cases cover dialog+prompt kinds/rounds, optional terminators, empty plugin-switch acknowledgement,+continuation envelopes, response/prompt/token bounds, embedded NUL rejection,+secure transport and explicit opt-in, callback exceptions/deadlines, premature+or disguised terminal OKs, principal pinning, mechanisms, wrong credentials,+second factors, wrong Kerberos identity, missing caches, TLS GSSAPI, repeated+native-context cleanup and cancellation before the server's mutual token.+A separate compiled non-threaded client proves that the runtime guard rejects+GSSAPI before native credential access.++Mac and exact-source Linux correctness record **844 PASS / 0 FAIL /+0 UNSUPPORTED / 0 unavailable**; all 793 published passes are preserved. All 173+policy tests pass. Linux smoke-tests all 39 additive benchmark paths. Complete+Mac runs measure 63 profiles seven times, including three new external-authentication+setup paths. The immutable first authentication reference is separate from all+previous references.++The timeout case is an approved test-contract correction, with explicit+retirement/rejection/fresh-connection assertions. Its original baseline/failure+history is preserved and it is excluded from the driver-improvement list. These+records cover the Mac/Linux client and isolated authentication services; they do+not configure host OS accounts or certify Windows SSPI or every PAM module/realm.+See [Stage 7](../docs/STAGE7_IMPLEMENTATION.md).+++The 11 Stage 7 review regressions cover wrapped/initial ERR-shaped plugin data,+malformed terminal OK/ERR redaction, preservation of actual server ERR and callback+exceptions, oversized clear-password challenges before credential callbacks,+header-only initial/continuation limits, smaller caller budgets and full query+budgets after authentication. Eight added policy tests cover partial-failure+classification/interruption, selective fixture setup, source-inventory routing,+private configuration, failed/interrupted container starts, diagnostic failures+and ownership collisions. See the [resolved review](../docs/STAGE7_CODE_REVIEW.md).
+ validation/MACOS.md view
@@ -0,0 +1,65 @@+# Managed MariaDB on macOS++Use **MariaDB 12.3 LTS** through Homebrew. It supplies a precompiled Apple Silicon package, the command-line tools and MariaDB Connector/C. Homebrew manages startup through launchd. [Homebrew package](https://formulae.brew.sh/formula/mariadb@12.3), [MariaDB installation guide](https://mariadb.com/docs/server/server-management/install-and-upgrade-mariadb/installing-mariadb/binary-packages/installing-mariadb-on-macos-using-homebrew)++## Install and verify the server++```sh+brew update+brew install mariadb@12.3+brew services start mariadb@12.3+export PATH="$(brew --prefix mariadb@12.3)/bin:$PATH"+mariadb --protocol=socket -e 'SELECT VERSION(), USER()'+```++The first startup can take several seconds. `brew services info mariadb@12.3` reports its status. If macOS asks to allow the new background item, allow the MariaDB service under System Settings, General, Login Items and Extensions.++The Homebrew installation creates a socket administrator matching your macOS username, so the verification command normally needs no root password or `sudo`. Use an application/test account for TCP connections. The versioned formula is keg-only; the `PATH` command above makes its tools available in the current shell. The scripts also recognize the standard Apple Silicon path directly.++On the initial development Mac, installation and both socket/TCP verification succeeded with MariaDB **12.3.3**, Homebrew bottle revision 1. The source revision and actual server settings are recorded again by each baseline run.++## Configure the validation environment++From this repository:++```sh+python3 validation/configure_macos.py+python3 validation/provision.py+```++The configuration helper requires the package to be installed first. It creates a private development CA and TLS certificates, enables row binlogs, allows 384 MiB packets for the inherited large-packet tests, pins default sessions to UTC and binds the server to **127.0.0.1**. It restarts the managed service and waits for readiness. It preserves the database directory and refuses to overwrite an unrelated `my.cnf`; in that situation merge the settings explicitly instead.++Provisioning creates only the reserved `mariadb_haskell_validation` and `testMySQLHaskell` databases and dedicated validation accounts. It can reset the passwords of those fixture accounts on repeated runs. The inherited suite requires its original localhost-only passwordless fixture account and database names; the new suite uses a separately generated password. Do not use these accounts for application data.++The native-password, ed25519, parsec and optional SHA-2 fixtures belong to MariaDB. MariaDB authentication mechanisms can retain historical `mysql` identifiers. PAM/GSSAPI need additional operating-system/realm setup and are recorded separately when that environment is unavailable.++On Apple Silicon the managed files are:++| Purpose | Location |+| --- | --- |+| Server and tools | `/opt/homebrew/opt/mariadb@12.3/bin` |+| Server configuration | `/opt/homebrew/etc/my.cnf` |+| Data directory | `/opt/homebrew/var/mysql` |+| Socket | `/tmp/mysql.sock` |+| Private development TLS files | `/opt/homebrew/var/mariadb-haskell/tls` |+| Test connection settings | `validation/.local/environment.json` in this repository |++## Start and stop it++```sh+brew services info mariadb@12.3+brew services stop mariadb@12.3+brew services start mariadb@12.3+brew services restart mariadb@12.3+```++Starting through `brew services` also registers startup at login. Stop the service when it is not needed. A system-wide binary installation does not require running the server as root.++## Keep test results comparable++```sh+brew update+brew outdated mariadb@12.3+```++Upgrade deliberately with `brew upgrade mariadb@12.3`, then restart the service and follow the package's database-upgrade instructions if required. Keep a backup before upgrading an instance containing valuable data. Capture a new version-specific baseline after an upgrade; never overwrite the old results or compare changed server versions as if only the driver changed.
+ validation/README.md view
@@ -0,0 +1,331 @@+# MariaDB validation and baseline++This suite tests the real Haskell driver against MariaDB. The first baseline must use the inherited implementation unchanged. Failures, unsupported APIs and unavailable environment fixtures remain visible; they are not converted into passing expected-failure tests.++**[Live results for the current tested code](RESULTS.md)** are committed on `main` and linked from the root README. Full runs update this document, its [colored summary](results/overview.svg) and [current.json](results/current.json) automatically. Status colors always include text labels; complete findings remain available in expandable sections. The original frozen baseline remains the comparison reference; Git history preserves earlier versions of the live report.++Install and verify the database **before** building or running the validation suite. [macOS setup](MACOS.md) uses a Homebrew binary package and its managed service. No MariaDB Server source build is needed.++## Run the suite++From the repository root, after the server setup:++```sh+python3 validation/provision.py+cabal update+python3 -m unittest discover -s validation -p 'test_*.py'+python3 validation/check.py+```++The `check.py` entry point verifies MariaDB before building, rebuilds the current source and invokes the recorder. This prevents stale executables being measured after source edits. The recorder checks connectivity, server identity, the reserved database, packet limits and binlog configuration. If preflight fails, it runs no tests or benchmarks. It then runs every new correctness case in a separate process, runs both inherited suites with their assertions preserved, seeds the reserved benchmark table, and interleaves seven repetitions of Haskell and matching MariaDB Connector/C workloads. Every measured process performs two warm-ups before recording timings. A hung/crashed case cannot stop the remaining new cases. `release_recorder.py` is the lower-level entry point for an already-built harness.+The original `run.py` remains byte-for-byte frozen; the release adapter changes+only its manifest lookup and self-package dependency exclusion. Both original+and adapter identities are recorded explicitly; benchmark bodies and policies+are unchanged.++Both MariaDB's default session timezone and the test-process timezone are pinned to UTC. This avoids seasonal timezone assumptions in inherited binlog tests. The runner verifies password-plugin and client-certificate fixtures with MariaDB's own client before attributing failures to the Haskell driver.++`validation/.local/environment.json` holds generated local connection settings and credentials with mode 0600. It is ignored by Git. The runner also accepts `--config FILE`. Haskell cases read the `MB_*` environment variables supplied by that file. No password is written into the baseline JSON or passed to the CLI on its command line.++Runs are retained under `validation/results/runs/`, including raw logs, machine/server/compiler details, resolved dependency versions, source/workload hashes, per-case outcomes, raw benchmark repetitions, operation latency samples, allocations and checksums. A summary is generated even when the driver has known failures. Initial capture exits nonzero on correctness failures; that does not invalidate its role as an honest baseline.++## Keep the live report current on main++For every change to driver code, tests, benchmarks or build inputs:++1. Commit the code being tested locally, so the run identifies its actual revision.+2. Run `python3 validation/check.py` with the complete suite and at least seven benchmark repetitions. It rebuilds, compares against the original baseline by default and refreshes `validation/RESULTS.md`, `validation/results/overview.svg` and `validation/results/current.json`, including when tests fail.+3. Run `python3 validation/publish_results.py --check`, then commit all three results files and push the code and results to `main` together.++The live report includes the tested commit, input hashes, environment, individual findings, raw samples and median performance changes. A subsequent results-only commit publishes it; that commit does not pretend that another run occurred. CI verifies the recorded inputs against the current files and flags a stale report. Documentation-only changes do not invalidate executable inputs.++A server/preflight/build/recorder failure updates the latest-attempt status. Any retained last-completed results are explicitly labeled as older, and the freshness check fails. Runs with filters, skipped suites/benchmarks or fewer than seven repetitions remain development runs and cannot replace the live full-suite report. Raw per-process logs stay in the ignored run directory; the committed JSON retains all completed measurements.++## Freeze the first baseline++Use a new directory name; existing baselines cannot be overwritten:++```sh+python3 validation/check.py \+  --freeze validation/baselines/macos-arm64-mariadb-12.3.3+```++Freezing requires the complete suite and at least seven repetitions. The driver source and workloads are hashed before and after the run. The baseline contains raw results, a readable summary and SHA-256 checksums. Raw per-process logs remain in the associated run directory. The inherited code under `src/` must still match the initial source tree when creating the first baseline.++## Compare every improvement++```sh+python3 validation/check.py \+  --compare validation/baselines/macos-arm64-mariadb-12.3.3/results.json+```++Comparison rejects removal of old cases, loss of a previous pass, hiding a failure as unsupported/unavailable, failed previously working benchmarks and a median slowdown over 20%. Newly passing cases are reported as improvements. A slowdown is a signal to repeat on an idle machine and investigate, not a precise statistical claim.++Speed comparisons require matching machine, server version/settings, compiler, resolved dependencies and benchmark workload hashes. This prevents comparing a changed workload or another machine to the Mac baseline. To compare only behavior across environments, add `--correctness-only-comparison`. Preserve the initial baseline; use a separate directory for another server/version/hardware profile. `--only SUBSTRING --no-upstream --no-benchmarks` is available for harness development but cannot freeze a baseline.++The initial [frozen baseline](baselines/macos-arm64-mariadb-12.3.3/SUMMARY.md) contains 300 executable checks, 11 missing-API entries and two unavailable-environment entries. The two inherited suites appear as aggregate records in JSON; their logs contain the individual 179 unit/property cases (one inherited external HTTPS ignore) and 32 integration cases.++## Scope and interpretation++### Stage 1 review runs and supplementary evidence++For the explicitly requested uncommitted review, use `python3 validation/check.py --working-tree`, followed by `python3 validation/publish_results.py --check --working-tree`. The report records the parent commit and hashes every actual build/test input. Normal CI and publication checks still reject dirty inputs. No command commits or pushes changes.++The frozen workloads, shared helpers, Connector/C source and recorder remain unchanged. `Validation.Stage1` adds deterministic transport fixtures, real loopback-proxy faults and live MariaDB cases. `stage1_gates.py` checks the 23 targeted original failures, preserves every original/last-accepted pass, rejects hidden failures, requires comparable complete evidence and flags timing/allocation increases above 5%. It does not turn a known deferred failure into a successful feature test. The accepted reference manifest is advanced only after human review, never by a failed attempt or an unreviewed working tree.++`python3 validation/supplementary.py --output PATH` builds first and records separate setup/parameter/transaction/batch measurements plus retention experiments. Its before-change reference covers only the original six supplementary profiles. New retention experiments have no invented before measurement: they sample current live heap after major GC at row checkpoints across 10,000 and 1,000,000 rows and hold a one-byte slice after a 16 MiB result. GC checkpoints occur inside those memory experiments; their timing is not comparable with frozen throughput workloads. Existing `max_live_bytes` remains explicitly labeled as a process high-water metric.++`python3 validation/profile_workloads.py --profiles PROFILE ... --output PATH` runs selected **unchanged** frozen workloads as separate development experiments. It requires a successful fresh build and stable input hashes; failures stop measurement. It never replaces the complete live report. All optimization experiments, including unsuccessful ones and the explicitly invalid stale-binary experiment, are retained for review. See [implementation evidence](../docs/STAGE1_IMPLEMENTATION_NOTES.md) for the final confirmation window and performance investigations.++[COVERAGE.md](COVERAGE.md) maps the cases, workloads and environment requirements. The suite covers the driver's relevant protocol, connection, value, SQL, transaction, streaming, error and replication surfaces. It is not a certification of every MariaDB storage engine, query optimizer plan, Galera topology or backup tool.++Capability inventory entries explicitly report `UNSUPPORTED` where the current driver has no API to exercise, including native bulk and metadata caching. These entries must become real tests and benchmarks when those APIs are added. They are not successful feature tests. Environment gaps remain separate from missing driver APIs.++The C control links the **installed MariaDB Connector/C** and consumes the same text-protocol SQL, ASCII fields, integer values and checksums. Binary prepared, vector, TLS, socket, concurrency and decoder microbenchmarks are currently Haskell-only and must not be presented as C comparisons. The write workload includes table truncation, a transaction, all inserts, commit and count/sum verification on both clients. Its operation latency is the whole transaction, not one insert.++Benchmark ns/unit uses rows for SELECT/INSERT, bytes for the 1 MiB BLOB workload and decoded rows for microbenchmarks. p50/p95/p99 operation latency uses complete queries, transactions or decoder batches. Large scans have few operation samples, so their tail percentiles are descriptive. RTS allocations/GC/live heap are Haskell metrics; unavailable C metrics are null. Connection and statement creation happen outside timing, including for concurrent readers. `-O2`, `+RTS -N4 -T`, warm-ups, repetition counts and durability settings are recorded.++## Stage 2 data installation and Linux profile++After a successful build, `check.py` uses the built library's Cabal Setup configuration to copy declared runtime data into its installation prefix. This tests the installed CA snapshot independently of the working directory and avoids stale package data. A copy failure or interruption is a visible failed attempt; no older binary/data combination is measured.++Linux integration is defined in `.github/workflows/mariadb-linux.yml` and `validation/linux/Dockerfile`. The pinned official MariaDB image and the current signed-index Cabal client provide disposable server, authentication, TLS and certificate fixtures. The source checkout must include Git history because the unchanged recorder verifies the original source revision. Run from the repository root:++```sh+docker build -f validation/linux/Dockerfile -t mariadb-haskell-linux .+docker run --name mariadb-haskell-linux --tmpfs /var/lib/mysql:size=2147483648 mariadb-haskell-linux+docker cp mariadb-haskell-linux:/workspace/validation/results/runs/stage2-linux ./validation/results/runs/+docker rm -v mariadb-haskell-linux+```++Use a new container name/output location for each retained run. Linux correctness requires every published Stage 1 pass, every Stage 2 native target and every additive Stage 2 case. Its results stay separate from the Mac live benchmark profile. The container uses its own database and private test CA; it does not install trust roots or change the host MariaDB service. Ensure sufficient Docker disk/build space; the local Stage 2 run used separate task-local build storage after encountering the existing Docker disk limit.++## Stage 3 statements and native feature benchmarks++Every complete `check.py` run now also runs eight native-feature profiles with+seven or more process repetitions. These compare native aggregate/unit bulk,+sequential prepared inserts, text batching and cached/uncached narrow/wide+prepared queries. Work, transaction boundaries and content verification match+within each comparison. Connection/statement setup is outside timing. Each+process performs two warm-ups. Wall time excludes the final GC; allocation,+CPU and GC counters include that collection consistently across alternatives.++The native recorder stores all samples and process logs under the complete run's+`native/` directory. Validated native evidence is embedded in the live JSON and+rendered in the main results document. Missing, partial, stale or inconsistent+native measurements block publication. A native failure updates the failed+attempt and retains the raw frozen-suite candidate, rather than presenting old+native measurements as current. Filtered development runs skip these timings.++The first native-feature capture is preserved separately under+`baselines/macos-arm64-native-statements/`; it does not replace the original+unchanged-driver baseline. Subsequent runs verify its checksum and compare only+matching native workloads, server, machine, compiler and dependencies. Native+timing increases above 20% fail the wrapper; increases above 5% in timing or+allocation remain explicit investigation signals. The accepted performance+reference is not advanced automatically.++For a separate native experiment, use a new output directory:++```sh+python3 validation/stage3_benchmarks.py --output validation/results/runs/NEW_NATIVE_RUN+```++The frozen native benchmark inventory slots remain historical placeholders;+they are not a statement that the new APIs are unsupported. The three correctness+inventory entries for bulk/cache/extended metadata now run actual feature tests.+See the [Stage 3 implementation and migration notes](../docs/STAGE3_IMPLEMENTATION.md)+and [continuation roadmap](../docs/ROADMAP.md). Linux CI also smoke-tests all eight+native benchmark paths without comparing their timings with Mac baselines.++The native baseline's `results.json`, `SUMMARY.md` and `SHA256SUMS` are required+and fingerprinted build/test inputs. Missing or changed artifacts fail validation.+Publication rereads the checksummed baseline and recalculates comparability,+deltas and alarms, so an incomplete or edited comparison cannot suppress a+regression. Matching uses the Haskell workload/fixture hashes, verified profile+units, compiler, dependencies, RTS, machine and server settings. Python admission+policy changes do not invalidate identical measured workloads. Dependency pairs+are normalized across JSON serialization.++## Stage 4 session reset, observations and reuse measurements++`getSessionState` exposes bounded observed schema/variables, transaction status+and the latest actual tracker envelope. `resetConnection` and+`resetConnectionWith` reset synchronized sessions, restore required state and+invalidate previous statements. See [API contracts and migration](../docs/STAGE4_IMPLEMENTATION.md).+The two former session-tracking/reset inventory entries now invoke real checks.+The previous-stage correctness reference protects all 462 published Stage 3 passes;+the original, accepted and first-native performance references remain unchanged.++Every full wrapper run also executes seven reuse profiles: reset versus reconnect+for TCP, TLS and Unix sockets, plus public snapshot reads. Reset/restoration and+statement preparation are inside timing, as are new connection setup and close+for reconnects. Both alternatives initialize and verify the same settings and+request result, then leave identical per-request state. Each process performs two+warmups; all seven repetitions, counters and checksums are retained under `reuse/`+and embedded in the live report. Snapshot measurements use a strict loop without+an allocated result list and represent hot repeated reads, not server round trips.++The first reuse capture is separately immutable under+`baselines/macos-arm64-session-reuse/`. Its results, summary and checksum manifest+are required fingerprinted inputs. The shared `feature_benchmarks.py` recorder+retains the existing native recorder's evidence checks and applies the same+checksum, workload, compiler, dependency, machine and server comparison rules to+reuse. Publication independently verifies both additive suites, including source+file maps and tested commits, rather than trusting recorded comparison flags.+A missing/failed/interrupted reuse phase publishes a failed attempt and preserves+the raw candidate; it cannot present older measurements as current.++Run a separate complete reuse experiment without replacing the live report:++```sh+python3 validation/stage4_benchmarks.py --output validation/results/runs/NEW_REUSE_RUN+```++The one-time `--freeze` option refuses an existing baseline directory. It is not+a way to advance a reference after a regression. The first capture precedes its+own checksummed artifact creation; later full runs fingerprint those artifacts.+Timing increases above 20% fail the wrapper, while timing/allocation increases+above 5% remain open investigations. Linux runs all new correctness cases and+smoke-tests these seven paths without comparing timings across platforms.++Stage 4 review fixtures include three dedicated role accounts and four roles,+created by `provision.py` on both Mac and Linux. One account has no default role;+the other reaches the reserved database through its configured default role only.+They test restoration order and quoted Unicode role names without altering the+ordinary benchmark user's role settings. Run provisioning before the new role+checks when updating an existing validation installation.++## Stage 5 CDC++[CDC API, migration, bounds and evidence](../docs/STAGE5_IMPLEMENTATION.md)+cover transaction-aware streaming, MariaDB GTID resume and manual semisync ACKs.+The two original CDC API inventory entries now execute real checks. A disposable+socket-only MariaDB fixture uses the installed binaries to test ACK waiting,+rotation, CRC32/NONE transitions, sparse images, schema change and actual restart.+It leaves the managed server's data/configuration unchanged and cleans up its+owned child processes and datadir.++Full runs require eight additional CDC profiles (47 profiles total), seven+process repetitions and two warmups per process. The separate first CDC capture+under `baselines/macos-arm64-cdc` is immutable; it does not replace an earlier+reference. Streaming retained heap checkpoints are mandatory, including warmups.+Use `python3 validation/stage5_benchmarks.py --output NEW_DIRECTORY` for a separate+complete CDC measurement. The full wrapper and publication guard independently+require current CDC evidence and verify all baseline deltas and heap checkpoints.++The [follow-up review](../docs/STAGE5_CODE_REVIEW.md) adds 58 regressions and+corrects temporal values, full GTID history and resource accounting. `CDCGtidList`+now exposes `GtidList` history and flags, separately from `GtidSet` resume state.+Two-phase ALTER and unknown GTID extension semantics fail before checkpoints;+they are not treated as ordinary transaction groups.++### Stage 6: streamed parameters and optional wire features++The four remaining API inventory rows invoke actual long-data, cursor, progress+and compression checks. [Stage 6 contracts](../docs/STAGE6_IMPLEMENTATION.md)+describe ownership, callback behavior, negotiation and resource limits. PAM and+GSSAPI remain separate authentication implementation/environment work; the+frozen timeout-reuse expectation still conflicts with safe retirement.++Every complete run adds thirteen independent profiles to the previous 47, for+**60 runnable profiles**, each with seven repetitions and two warmups. Use+`python3 validation/stage6_benchmarks.py --output NEW_DIRECTORY` for a separate+complete capture. The first capture must use `--freeze` once to create+`baselines/macos-arm64-wire-features`; later captures refuse to replace that+reference. Existing references are unchanged.+Upload units are bytes, cursor/transport units are rows, and progress units are+complete queries. All alternatives check consumed/stored values. Cursor and+compression costs are reported even when they are slower than ordinary reads.+Progress throughput measures negotiation with no emitted reports; real report+delivery, callback failure and cancellation have separate correctness coverage.++**Review checkpoint, 9 November 2026:** all follow-up findings are fixed.+Complete Mac and exact-source Linux correctness record **793 PASS / 1 known+timeout-contract FAIL / 0 UNSUPPORTED / 2 unavailable**. All 95 Stage 6 cases+and 155 policy tests pass. Two complete Mac captures each measure all 60 profiles+with seven repetitions and exit 0; the second supplies the live report.+The first wire-feature reference is frozen independently. Its fresh-chunk+64 MiB profile checks all twelve checkpoints per process; a separate buffering+negative control must be rejected by the actual guard. Every checkpoint is+checked against the streaming allowance. Six ordinary timing/reference, two+reuse and three wire-feature timing signals remain open in capture 2; all+allocation investigation lists are empty. No existing reference advances.+See the [confirmation manifest](results/stage6-confirmation.json),+[resolved review](../docs/STAGE6_CODE_REVIEW.md) and+[final checkpoint](../docs/STAGE6_IMPLEMENTATION.md#final-review-fix-validation--9-november-2026).+++**Committed publication, 9 November 2026:** the [live report](RESULTS.md) now+records the complete run on source commit `9c4bac0`: 793 PASS / 1 known FAIL /+0 UNSUPPORTED / 2 unavailable and all 60 runnable profiles with seven repetitions.+Correctness/evidence gates pass; the run exits 1 for timing alarms. All references+and earlier review captures remain unchanged. The+[publication manifest](results/stage6-publication.json) also retains the stopped+extra confirmation and identifies the completed capture used by the live report.+++### Stage 7: PAM, GSSAPI and the corrected timeout contract++Full `check.py` runs now prepare an owned loopback Docker authentication fixture+before building, after verifying the managed MariaDB server. Docker must be+running. The fixture supplies real PAM conversations and a disposable Kerberos+realm, server principal/keytabs, pre-acquired client tickets, TLS CA and MariaDB+server. It changes no host PAM policy, OS account, default ticket cache or trust+store, and removes its own container/private files on exit. Explicit existing+`MB_AUTH_*` fixture settings are reused instead. Missing requested fixtures fail+real checks; the two old unavailable placeholders are gone.++The full workflow keeps all 60 earlier profiles and adds three authentication+setup profiles, each with two warmups and seven measured processes of ten+connections. Each operation creates TLS parameters/loads the fixture CA where+applicable, connects, authenticates, initializes session state, verifies SELECT+and closes. PAM uses TLS; GSSAPI uses a pre-acquired service ticket over TCP.+These are distinct end-to-end setup workloads, not interchangeable authentication+microbenchmarks. Applications may reuse TLS parameters. KDC ticket acquisition+is excluded. The separate first authentication baseline is immutable, and a+fixture-image change withholds timing deltas. Current measurements are embedded+in the live JSON and independently required by publication freshness checks.++The original timeout test now asserts Broken status, rejection of further commands+and a healthy fresh connection after cancellation, following Anton's explicit+9 November approval. It keeps the original ID, SQL and timeout. The report labels+this contract correction separately from driver improvements; the original+baseline and failure evidence remain unchanged. See [contracts and deployment+requirements](../docs/STAGE7_IMPLEMENTATION.md), including GSSAPI's threaded-RTS+requirement and Linux native-library dependency.+++Stage 7 review fixes enforce a separate authentication framing budget before+body reads, then return to the ordinary query reader after login. Malformed+packet diagnostics are redacted without changing application callback exceptions+or genuine SQL ERR replies. Focused non-live filters (for example+`--only=protocol.length-0 --no-upstream --no-benchmarks`) no longer provision+Docker/Kerberos/PAM. Full runs and selected live authentication cases still require+the real services. Partial setup failures never replace full-run attempt status,+including `--only=...` and runs with fewer than seven repetitions. Owned fixture+containers are identified before creation and verified by a unique instance label+before cleanup, including failed or interrupted starts.++## Independent package release++The package identity is now `mariadb-haskell`. The checkout's `cabal.project`+opts into the developer-only `validation` flag and optimization level 2. The+Hackage source archive omits that project and defaults the flag off. Explicit+archive/developer builds can use `cabal build exe:mariadb-validation -fvalidation -O2`.+Run the frozen recorder through `python3 validation/release_recorder.py`, not+by invoking historical `run.py` directly. The adapter is digest-pinned to the+original recorder and records exactly two metadata substitutions, plus its own+entry point and hashes. The report publication guard verifies this provenance.+No frozen baseline, workload, sampler or comparison policy is rewritten.++See [release preparation](../docs/RELEASING.md) for clean source-archive checks+and the distinction between local artifacts, a Hackage candidate and publication.++Linux correctness containers can keep large Cabal/build caches in task-owned+host directories mounted at `/root/.cabal` and `/workspace/dist-newstyle`. Do+not mount a host Cabal directory containing Hackage credentials. The image+initializes its own HTTPS configuration at runtime. The managed database and+disposable fixture directories use temporary storage; remove owned containers+with `docker rm -v` when not using `--rm`, to avoid orphaning image-declared+anonymous database volumes. Retain evidence before removing a container.
+ validation/RESULTS.md view
@@ -0,0 +1,300 @@+# Live MariaDB validation results++Current measured source, compared with the original unchanged-driver baseline. Uncommitted review builds are identified by input fingerprints below.++![Validation overview: passing records, failures, unsupported APIs and environment gaps](results/overview.svg)++[Correctness](#correctness) · [Benchmarks](#benchmarks) · [Run details](#run-details) · [Raw results](results/current.json) · [Original baseline](baselines/macos-arm64-mariadb-12.3.3/SUMMARY.md)++## Implementation acceptance++Stage 1 targeted failures now passing: **23 / 23**. Original-baseline and last-accepted correctness protection: **PASS**.++Timing investigations above 5%: **51**; allocation investigations above 5%: **0**. These are investigation signals, not automatically accepted costs.++The frozen baseline retains its original meaning in all comparison tables. The last-accepted reference separately prevents an improvement being lost. [Acceptance evidence](results/stage1-gates.json) · [Confirmation history](results/stage1-confirmation.json) · [Supplementary evidence](results/references/stage1-after-final.json) · [Review notes](../docs/STAGE1_IMPLEMENTATION_NOTES.md).++Confirmation and supplementary files identify their own input fingerprints. Evidence with a different fingerprint is historical and does not confirm the current source.++Stage 4 session/reset targets passing: **2 / 2**. [Session reuse and observations](../docs/STAGE4_IMPLEMENTATION.md).++Stage 3 prepared/native targets passing: **5 / 5**. [Roadmap and continuation](../docs/ROADMAP.md) · [Stage 3 implementation and validation](../docs/STAGE3_IMPLEMENTATION.md).++Stage 2 native-connection targets passing: **14 / 14**. Every PASS from the previous published stage is separately protected. [Stage 2 implementation and validation](../docs/STAGE2_IMPLEMENTATION.md).++> [!WARNING]+> **Performance acceptance remains open.** Timing/allocation signals require investigation across the entire confirmation window; they have not been waived or accepted automatically.++> [!NOTE]+> **Approved test-contract correction:** `reliability.timeout-before-metadata` now asserts safe retirement, rejection of further commands and a healthy fresh connection after cancellation. Its former reuse expectation contradicted the passing retirement test. The original baseline and failure history remain unchanged. This passing assertion is excluded from the driver-improvement list below. [Contract and evidence](../docs/STAGE7_IMPLEMENTATION.md).++**Baseline comparison: 🟠 Attention required** · 1 regression signal · 68 newly passing checks.++A passing comparison does not mean all functionality passes.++<details open>+<summary>🟠 Comparison signals — investigation required</summary>++- haskell/latency.prepared: median slowed by more than 20%; confirm on an idle machine++</details>++<details>+<summary>🟢 Newly passing checks</summary>++- `protocol.continuation-sequence-rejected`+- `protocol.large-row-is-not-eof`+- `protocol.unsigned-last-insert-id`+- `protocol.column-definition-roundtrip`+- `protocol.binary-plugin-challenge`+- `protocol.unknown-plugin-rejected`+- `protocol.native-extension-handshake`+- `protocol.prepared-out-capability`+- `protocol.empty-packet-predicate`+- `protocol.text-field-trailing-data`+- `protocol.text-field-integer-overflow`+- `protocol.reset-does-not-consume-pending-row`+- `connection.default-full-unicode`+- `auth.ed25519.tcp`+- `auth.ed25519.tls`+- `auth.ed25519.socket`+- `auth.parsec.tcp`+- `auth.parsec.tls`+- `auth.parsec.socket`+- `auth.sha2.tcp`+- `auth.sha2.socket`+- `sql.procedure-prepared-out`+- `sql.zero-date-preservation`+- `sql.unsigned-generated-id`+- `parameters.literal-question-mark`+- `parameters.comment-question-mark`+- `parameters.no-backslash-text`+- `packets.live-blob-16777216`+- `packets.live-blob-16777217`+- `packets.live-blob-33554430`+- `transaction.partial-result-exception`+- `transaction.nested-preserves-outer`+- `reliability.reset-unread-result`+- `reliability.server-kill-query`+- `reliability.tls-cancellation-propagates`+- `connection.localhost-address-fallback`+- `types.non-utf8-results`+- `capability.binary-bulk`+- `capability.metadata-cache`+- `capability.extended-metadata`+- `capability.session-tracking`+- `capability.reset-connection`+- `capability.progress-reporting`+- `capability.cursor-fetch`+- `capability.long-data`+- `capability.compression`+- `auth.mutual-tls-helper-client`+- `connection.mozilla-ca-store-installed`+- `auth.pam-environment`+- `auth.gssapi-environment`+- `cdc.multiple-table-maps`+- `cdc.repeated-row-events`+- `cdc.table-id-mismatch`+- `cdc.crc32-corruption-rejected`+- `cdc.gtid-resume`+- `cdc.durable-ack-control`+- `cdc.native-event-160`+- `cdc.native-event-161`+- `cdc.native-event-162`+- `cdc.native-event-163`+- `cdc.native-event-164`+- `cdc.native-event-165`+- `cdc.native-event-166`+- `cdc.native-event-167`+- `cdc.native-event-168`+- `cdc.native-event-169`+- `cdc.native-event-170`+- `cdc.native-event-171`++</details>++| Reference | Commit |+| --- | --- |+| **Baseline** · original unchanged-driver run | `5deb205d5835` |+| **Current** · latest completed run | `6bbcf13fa3b6` |++Both comparison tables use these same two runs.++## Correctness++Counts include two aggregate records for the inherited unit/property and integration suites; see [coverage](COVERAGE.md). Change = Current − Baseline, in check records.++| Status | Baseline | Current | Change vs baseline |+| --- | ---: | ---: | ---: |+| 🟢 PASS | 246 | **844** | 🟢 +598 |+| 🔴 FAIL | 56 | **0** | 🟢 -56 |+| 🟠 TIMEOUT | 0 | **0** | 0 |+| 🔵 UNSUPPORTED | 11 | **0** | -11 |+| 🟡 ENVIRONMENT_UNAVAILABLE | 2 | **0** | -2 |++## Benchmarks++**Lower time is faster.** Both timing columns show median **ns/unit**; p95 describes complete operations in the Current run. [Coverage](COVERAGE.md) defines each workload's unit.++**Change = 100 × (Current − Baseline) / Baseline**, using unrounded medians. 🟢 Negative = faster · 🟡 Positive = slower · 🔴 More than 20% slower = investigate. ⚪ Zero at displayed precision = unchanged.++> [!NOTE]+> A benchmark marked PASS completed its workload. Its timing can still trigger a slowdown alert.++> [!NOTE]+> Native bulk execution and metadata caching are implemented. The two original native benchmark inventory slots remain frozen historical placeholders; current feature measurements and same-build comparisons are recorded in the [Stage 3 evidence](../docs/STAGE3_IMPLEMENTATION.md).++Environment, compiler, dependencies and benchmark workloads match the original baseline.++### Native MariaDB features++Same-build alternatives below use identical data, transactions and checksums. These new feature comparisons are separate from the original frozen baseline. Lower time/allocation is better; changes are `(current / baseline - 1) × 100`.++| Comparison | Baseline ns/unit | Current ns/unit | Change vs baseline | Allocation change |+| --- | ---: | ---: | ---: | ---: |+| `native_vs_sequential` | 41,574.2 | 1,966.4 | -95.3% | -91.1% |+| `native_vs_text_batch` | 11,001.4 | 1,966.4 | -82.1% | -77.9% |+| `unit_vs_aggregate` | 1,966.4 | 2,185.6 | +11.1% | +49.1% |+| `cached_vs_uncached_narrow` | 39,226.0 | 37,980.0 | -3.2% | -15.9% |+| `cached_vs_uncached_wide` | 76,824.0 | 56,378.0 | -26.6% | -73.5% |++Native timing units are rows for bulk inserts and complete queries for cached/uncached reads. Seven or more independent processes per profile each perform two warm-ups; all samples, CPU/GC counters and checksums are embedded in [current.json](results/current.json).++Native feature baseline comparison: **comparable**; timing investigations above 5%: **0**; allocation investigations: **0**. [Immutable native baseline](baselines/macos-arm64-native-statements/results.json).++### Session reuse++Baseline = reconnect, initialize, prepare, execute and close. Current = reset, restore, prepare and execute the same request on one connection. All application settings and absence of prior-request data are verified; preparation and restoration are inside timing. Changes are `(current / baseline - 1) × 100`; lower is better.++| Transport | Baseline ns/request | Current ns/request | Change vs baseline | Allocation change |+| --- | ---: | ---: | ---: | ---: |+| `tcp` | 510,800.0 | 367,900.0 | -28.0% | -16.8% |+| `tls` | 38,469,366.7 | 464,833.3 | -98.8% | -77.1% |+| `socket` | 357,466.7 | 275,566.7 | -22.9% | -14.8% |++Public snapshot read: **3.5 ns/call**, **0.0 allocated bytes/call**, no SQL. Seven or more independent processes per profile, with two warmups; raw samples and checksums are embedded in [current.json](results/current.json).++Immutable reuse baseline comparison: **comparable**; timing investigations above 5%: **4**; allocation investigations: **0**. [First reuse baseline](baselines/macos-arm64-session-reuse/results.json).++### MariaDB CDC++[CDC contracts and limits](../docs/STAGE5_IMPLEMENTATION.md) describe transaction checkpoints and explicit acknowledgements. XA-aware checkpointing and storage-engine-binlog GTID semisync remain outside this implementation.++Separate immutable first CDC capture; no equivalent CDC throughput measurement exists in the unchanged-driver baseline. Units are rows, except framing/CRC uses events. Live profiles include source connection/setup and checkpoint observation; source log creation is outside timing. Retention timing includes four forced major collections and is a memory workload.++| Workload | Baseline ns/unit | Current ns/unit | Change vs baseline | Current allocated B/unit |+| --- | ---: | ---: | ---: | ---: |+| `cdc.decode.single-table` | 454.9 | 465.7 | +2.4% | 3,720.5 |+| `cdc.decode.interleaved` | 537.8 | 513.2 | -4.6% | 3,744.3 |+| `cdc.decode.sparse-update` | 805.0 | 806.6 | +0.2% | 4,465.9 |+| `cdc.decode.compressed` | 1,897.0 | 1,953.5 | +3.0% | 43,745.6 |+| `cdc.framing.crc32` | 1,242.5 | 1,148.0 | -7.6% | 17,823.8 |+| `cdc.live.rows` | 4,192.0 | 4,251.0 | +1.4% | 19,094.4 |+| `cdc.live.checkpoints` | 13,890.0 | 13,670.0 | -1.6% | 32,594.9 |+| `cdc.retention` | 640.0 | 641.0 | +0.2% | 8,688.0 |++Streaming retained heap at 1,000,000 rows: **86,240–86,240 bytes** after major GC across the measured processes. All warmup/measured checkpoints are retained in [current.json](results/current.json). This is live heap, not a process high-water mark.++CDC comparison: **comparable**; timing investigations above 5%: **0**; allocation investigations: **0**. [First CDC capture](baselines/macos-arm64-cdc/results.json).++### Streamed parameters and optional wire features++Same-build alternatives use identical values and checksums. Lower time/allocation is better; change is `(current / baseline - 1) × 100`. Upload units are bytes, cursor/transport units are rows, and progress units are complete SELECT queries. Preparation/connection setup are excluded. Progress measures the negotiated idle-callback path; real progress delivery is tested separately.++| Comparison | Baseline ns/unit | Current ns/unit | Change vs baseline | Allocation change |+| --- | ---: | ---: | ---: | ---: |+| `streamed upload` | 5.16 | 5.19 | +0.5% | +97.0% |+| `cursor batch 32` | 372.80 | 2,023.10 | +442.7% | +49.4% |+| `cursor batch 512` | 372.80 | 858.70 | +130.3% | +4.6% |+| `cursor batch 4096` | 372.80 | 774.10 | +107.6% | +1.2% |+| `narrow compression` | 341.50 | 747.50 | +118.9% | +4.9% |+| `wide compression` | 4,744.00 | 10,539.00 | +122.2% | +42.9% |+| `progress negotiation` | 41,960.00 | 42,392.00 | +1.0% | +0.8% |++Server cursors materialize results and add fetch round trips. Compression is optional and trades CPU/allocations for fewer network bytes; local timing is not a WAN result. These features need not outperform ordinary streaming on this Mac.++[First wire-feature capture](baselines/macos-arm64-wire-features/results.json): **comparable**; timing investigations above 5%: **6**; allocation investigations: **0**. All seven-repetition samples are retained in [current.json](results/current.json).++### External authentication setup++Complete connect/authenticate/session-initialize/query/close costs; units are verified connections. PAM uses TLS and GSSAPI uses a pre-acquired Kerberos service ticket over TCP. KDC ticket acquisition is excluded. The fixture image identity must match before timing deltas are compared.++| Profile | Baseline ns/connection | Current ns/connection | Change vs baseline | Allocation change |+| --- | ---: | ---: | ---: | ---: |+| `auth.pam.password` | 45,259,300.00 | 44,998,800.00 | -0.6% | +0.1% |+| `auth.pam.multiple` | 45,725,900.00 | 44,843,500.00 | -1.9% | +0.1% |+| `auth.gssapi.cached-ticket` | 3,941,800.00 | 4,279,400.00 | +8.6% | +1.1% |++[First authentication capture](baselines/macos-arm64-external-authentication/results.json): timing investigations **1**; allocation investigations **0**. These authentication alternatives have different security/transport work and are not compared against one another.++### Haskell driver++| Workload | Baseline (ns/unit) | Current (ns/unit) | Change vs baseline | Current p95 (ms) | Current status |+| --- | ---: | ---: | --- | ---: | --- |+| `blob.prepared` | 3.1 | **3.3** | 🟡 +4.4% slower | 4.300 | 🟢 PASS |+| `blob.text` | 3.2 | **3.3** | 🟡 +2.6% slower | 4.241 | 🟢 PASS |+| `latency.prepared` | 41826.0 | **51584.0** | 🔴 +23.3% slower | 0.088 | 🟢 PASS |+| `latency.text` | 39354.0 | **44460.0** | 🟡 +13.0% slower | 0.079 | 🟢 PASS |+| `micro.binary-row` | 246.1 | **255.4** | 🟡 +3.8% slower | 26.289 | 🟢 PASS |+| `micro.text-row` | 313.2 | **308.9** | 🟢 -1.4% faster | 31.596 | 🟢 PASS |+| `native.bulk` | — | — | — | — | 🔵 UNSUPPORTED |+| `native.metadata-cache` | — | — | — | — | 🔵 UNSUPPORTED |+| `read.parallel-16` | 107.6 | **107.7** | 🟡 +0.1% slower | 19.250 | 🟢 PASS |+| `read.parallel-4` | 118.0 | **134.2** | 🟡 +13.8% slower | 7.445 | 🟢 PASS |+| `read.prepared.narrow` | 314.6 | **351.4** | 🟡 +11.7% slower | 5.067 | 🟢 PASS |+| `read.prepared.wide` | 2425.1 | **2583.9** | 🟡 +6.6% slower | 27.358 | 🟢 PASS |+| `read.socket.narrow` | 339.2 | **386.3** | 🟡 +13.9% slower | 4.758 | 🟢 PASS |+| `read.text.narrow` | 305.5 | **349.9** | 🟡 +14.5% slower | 3.802 | 🟢 PASS |+| `read.text.wide` | 2876.4 | **2913.1** | 🟡 +1.3% slower | 31.608 | 🟢 PASS |+| `read.tls.narrow` | 370.6 | **409.7** | 🟡 +10.6% slower | 5.826 | 🟢 PASS |+| `read.vector.narrow` | 365.8 | **371.0** | 🟡 +1.4% slower | 4.171 | 🟢 PASS |+| `write.execute-many` | 9670.0 | **9367.0** | 🟢 -3.1% faster | 9.709 | 🟢 PASS |+| `write.prepared` | 39408.0 | **40821.0** | 🟡 +3.6% slower | 41.394 | 🟢 PASS |+| `write.text` | 38324.0 | **43576.0** | 🟡 +13.7% slower | 47.089 | 🟢 PASS |++### MariaDB Connector/C control++| Workload | Baseline (ns/unit) | Current (ns/unit) | Change vs baseline | Current p95 (ms) | Current status |+| --- | ---: | ---: | --- | ---: | --- |+| `blob.text` | 3.0 | **3.2** | 🟡 +5.6% slower | 4.632 | 🟢 PASS |+| `latency.text` | 26568.0 | **28256.0** | 🟡 +6.4% slower | 0.059 | 🟢 PASS |+| `read.text.narrow` | 197.4 | **213.2** | 🟡 +8.0% slower | 2.343 | 🟢 PASS |+| `read.text.wide` | 1261.0 | **1346.0** | 🟡 +6.7% slower | 15.163 | 🟢 PASS |+| `write.execute-many` | 7635.0 | **8581.0** | 🟡 +12.4% slower | 9.124 | 🟢 PASS |+| `write.text` | 28478.0 | **31197.0** | 🟡 +9.5% slower | 31.680 | 🟢 PASS |++— means no successful measurement is available, or the runs cannot be compared for Change.++Each measured process has two warm-ups; this run has **7 repetitions** per profile. All samples, allocations, GC statistics, checksums and latency measurements are retained in [current.json](results/current.json).++## Run details++<details>+<summary>Revision, environment and reproducibility</summary>++| Field | Latest attempt |+| --- | --- |+| State / exit code | COMPLETED / 1 |+| Parent commit (working-tree inputs are identified separately) | [`6bbcf13fa3b66d96f26c8c4b36f5d69d5f69c25f`](https://github.com/aantich/mariadb-haskell/commit/6bbcf13fa3b66d96f26c8c4b36f5d69d5f69c25f) |+| Branch | `main` |+| Started / finished (UTC) | 2026-11-09T15:39:49.706665+00:00 / 2026-11-09T15:43:16.106989+00:00 |+| Input fingerprint | `23135d7aba3c73c1b39691f9b6ae3ea21104c45838d753a47ce9fa6a73892153` |+| Local logs and build plan | `validation/results/runs/release-0.1.0.0-final-ci` (ignored by Git) |+| Server of completed run | 12.3.3-MariaDB-log |+| Host | Apple M4 Max; macOS 26.4 |+| Compiler / build tool | GHC 9.10.3; Cabal 3.16.1.0 |+| MariaDB Connector/C | 3.4.10 |+| Complete suite / repetitions | True / 7 |+| Driver/workloads stable during execution | True |+| Frozen recorder source | `e756bc0f593b54bc9207fd88d2a53120a4654832650d0a148555a69ebd413346` |+| Package metadata adapter | `082bee446a000e93bd0e55904134f49ce6f5fc1f4328f40d8e94443bbd99a9e7` |+| Adapted recorder body | `6a7d130df9ad7421f83940b3b6f6daf8e3de9ac6a3e7a3f91d357766e062b16f` |++The independent package uses a separately fingerprinted metadata adapter. It substitutes only the manifest filename and self-package exclusion in the digest-pinned original recorder; entry-point dispatch is handled by the adapter. Frozen workload bodies, sampling and comparison policy remain unchanged. The original file hash is not presented as the adapter's hash.+++Committed runs identify their tested revision. An uncommitted review run identifies its parent revision plus SHA-256 hashes of the actual working-tree inputs; it does not claim the parent commit contains these changes. A following results-only commit does not claim another test run. CI checks that the report, graphic and raw results match the current inputs.++Full `python3 validation/check.py` runs refresh this live document, [overview.svg](results/overview.svg) and [current.json](results/current.json), including failed attempts. Commit all three with each tested change. Git history retains previous results.++</details>
+ validation/auth_fixture.py view
@@ -0,0 +1,181 @@+#!/usr/bin/env python3+"""Manage a disposable external-authentication fixture and private client files.++The normal Mac workflow uses a container. Linux CI runs the same services inside+its already disposable container. Neither mode changes host PAM/realm settings.+"""+from contextlib import contextmanager+import json+import hashlib+import os+from pathlib import Path+import shutil+import subprocess+import tempfile+import time+import uuid++HERE = Path(__file__).resolve().parent+IMAGE = 'mariadb-haskell-stage7-auth'++++def runtime_identity(image):+    """Hash runtime files/configuration, excluding changing provenance indexes."""+    config_keys = ('Env', 'Entrypoint', 'Cmd', 'WorkingDir', 'User', 'ExposedPorts',+                   'Volumes', 'Labels', 'StopSignal', 'Healthcheck', 'OnBuild',+                   'Shell', 'ArgsEscaped')+    fields = {key: image.get(key) for key in ('Architecture', 'Os', 'Variant', 'RootFS')}+    fields['Config'] = {key: image.get('Config', {}).get(key) for key in config_keys}+    # OCI omits the default working directory; Docker inspect spells it ''.+    fields['Config']['WorkingDir'] = fields['Config']['WorkingDir'] or ''+    return 'docker-runtime-sha256:'+hashlib.sha256(json.dumps(fields,+        sort_keys=True, separators=(',', ':')).encode()).hexdigest()+++def command(args, **kwargs):+    return subprocess.run(list(map(str, args)), check=True, **kwargs)+++def cleanup_container(owner):+    """Remove only a container bearing this invocation's unique ownership label.++    Docker can create a container before run reports a startup error. Probe the+    registered name even on that failure path; a name collision is not ownership.+    Diagnostic failures must never prevent the removal attempt.+    """+    inspected = subprocess.run(['docker', 'inspect', '--format',+        '{{ index .Config.Labels "mariadb-haskell.fixture-owner" }}', owner],+        capture_output=True, text=True, check=False, timeout=15)+    if inspected.returncode or inspected.stdout.strip() != owner:+        return+    try:+        subprocess.run(['docker', 'logs', owner], check=False, timeout=15)+    except (OSError, subprocess.SubprocessError):+        pass+    finally:+        subprocess.run(['docker', 'rm', '-f', owner], check=True,+                       stdout=subprocess.DEVNULL, timeout=30)+++@contextmanager+def environment(*, local=False):+    """Yield fixture-specific environment and clean up only owned resources.++    The caller must keep this scope alive throughout validation. Credentials,+    caches and CA files are private temporary files, never report artifacts.+    """+    (HERE/'.local').mkdir(exist_ok=True)+    with tempfile.TemporaryDirectory(prefix='external-auth-', dir=HERE/'.local') as directory:+        private = Path(directory)+        owner = 'mariadb-haskell-auth-'+uuid.uuid4().hex[:12]+        process = None+        container = None+        fixture_root = Path('/fixture')+        try:+            if local:+                # This branch is for the disposable Linux CI image only.+                if not Path('/opt/driver-auth/disposable-fixture').is_file() or not Path('/opt/driver-auth/pam_fixture.so').is_file():+                    raise RuntimeError('local authentication fixtures require the disposable Linux CI image')+                fixture_root = private/'services'+                process = subprocess.Popen(['python3', str(HERE/'auth_fixture/serve.py')],+                    env={**os.environ, 'MB_AUTH_FIXTURE_ROOT': str(fixture_root),+                         'MB_AUTH_FIXTURE_PORT': '3307'})+                port, kdc_port = 3307, 10088+            else:+                command(['docker', 'build', '-t', IMAGE, '-f', HERE/'auth_fixture/Dockerfile', HERE/'auth_fixture'])+                # Register before the fallible creation call: the daemon may+                # have created the resource when its client fails or is killed.+                container = owner+                command(['docker', 'run', '-d', '--name', owner,+                    '--label', 'mariadb-haskell.fixture=external-authentication',+                    '--label', 'mariadb-haskell.fixture-owner='+owner,+                    '--tmpfs', '/fixture:rw,mode=0700,size=536870912',+                    '-p', '127.0.0.1::3306', '-p', '127.0.0.1::10088', IMAGE],+                    stdout=subprocess.DEVNULL)+                def published_port(number):+                    value = subprocess.check_output(['docker', 'port', owner, f'{number}/tcp'], text=True).strip()+                    if not value.startswith('127.0.0.1:'):+                        raise RuntimeError('authentication fixture must be bound to loopback')+                    return int(value.rsplit(':', 1)[1])+                port, kdc_port = published_port(3306), published_port(10088)+            for _ in range(160):+                if local:+                    ready = (fixture_root/'ready.json').is_file()+                    alive = process.poll() is None+                else:+                    ready = subprocess.run(['docker', 'exec', owner, 'test', '-f', '/fixture/ready.json'],+                                           capture_output=True).returncode == 0+                    alive = subprocess.check_output(['docker', 'inspect', '-f', '{{.State.Running}}', owner], text=True).strip() == 'true'+                if ready:+                    break+                if not alive:+                    raise RuntimeError('external authentication fixture stopped before readiness')+                time.sleep(0.25)+            else:+                raise RuntimeError('external authentication fixture readiness deadline exceeded')+            for filename in ['ready.json', 'tls-cert.pem', 'driver.ccache', 'other.ccache']:+                if local:+                    shutil.copyfile(fixture_root/filename, private/filename)+                else:+                    # Docker's archive endpoint can omit tmpfs mount contents.+                    # Read through the owned container process instead; ticket+                    # bytes go directly into private files, never tool output.+                    (private/filename).write_bytes(subprocess.check_output(+                        ['docker', 'exec', owner, 'cat', '/fixture/'+filename]))+                (private/filename).chmod(0o600)+            metadata = json.loads((private/'ready.json').read_text())+            realm = metadata['realm']+            (private/'krb5.conf').write_text(f'''[libdefaults]+ default_realm = {realm}+ dns_lookup_kdc = false+ dns_lookup_realm = false+ rdns = false+ udp_preference_limit = 1+[realms]+ {realm} = {{+  kdc = 127.0.0.1:{kdc_port}+ }}+''')+            if local:+                identity = 'local-disposable-linux-profile'+            else:+                image_id = subprocess.check_output(['docker', 'inspect', '-f', '{{.Image}}', container], text=True).strip()+                image = json.loads(subprocess.check_output(['docker', 'image', 'inspect', image_id], text=True))[0]+                identity = runtime_identity(image)+            yield {'MB_AUTH_FIXTURE_IMAGE_ID': identity,+                'MB_AUTH_HOST': '127.0.0.1', 'MB_AUTH_PORT': str(port),+                'MB_AUTH_TLS_CA': str(private/'tls-cert.pem'),+                'MB_AUTH_PAM_PASSWORD': metadata['password'], 'MB_AUTH_PAM_CODE': metadata['code'],+                'MB_AUTH_GSSAPI_PRINCIPAL': metadata['principal'],+                'MB_AUTH_GSSAPI_OTHER_CACHE': 'FILE:'+str(private/'other.ccache'),+                'KRB5CCNAME': 'FILE:'+str(private/'driver.ccache'),+                'KRB5_CONFIG': str(private/'krb5.conf')}+        finally:+            if container:+                # Exact generated name only. Never prune unrelated containers,+                # images, volumes, builder caches or the developer's databases.+                cleanup_container(container)+            if process:+                process.terminate()+                try:+                    process.wait(timeout=15)+                except subprocess.TimeoutExpired:+                    process.kill()+                    process.wait()+++def main():+    """Run a command with private fixture variables (use -- before arguments)."""+    import sys+    args = sys.argv[1:]+    if args and args[0] == '--':+        args = args[1:]+    if not args:+        raise SystemExit('usage: python3 validation/auth_fixture.py -- COMMAND [ARGUMENT ...]')+    with environment() as fixture:+        return subprocess.call(args, env={**os.environ, **fixture})+++if __name__ == '__main__':+    raise SystemExit(main())
+ validation/auth_fixture/.dockerignore view
@@ -0,0 +1,1 @@+__pycache__
+ validation/auth_fixture/Dockerfile view
@@ -0,0 +1,12 @@+# Authentication services only; the client under test runs on the host.+FROM mariadb:12.3@sha256:d46e102fa8b9b72bfcdeb56f9dd538742092119dc8b11f31fe662605cd234cd9+USER root+RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \+    krb5-kdc krb5-admin-server krb5-user libpam0g-dev gcc libc6-dev python3 openssl \+    mariadb-plugin-gssapi-server \+    && rm -rf /var/lib/apt/lists/*+COPY pam_fixture.c /opt/driver-auth/pam_fixture.c+RUN gcc -Wall -Wextra -Werror -fPIC -shared /opt/driver-auth/pam_fixture.c \+    -lpam -o /opt/driver-auth/pam_fixture.so+COPY serve.py /opt/driver-auth/serve.py+ENTRYPOINT ["python3", "/opt/driver-auth/serve.py"]
+ validation/auth_fixture/pam_fixture.c view
@@ -0,0 +1,45 @@+/* A deterministic PAM module for a disposable test server. These are synthetic+ * fixture credentials; no host OS account or password database is consulted. */+#include <security/pam_modules.h>+#include <security/pam_appl.h>+#include <stdlib.h>+#include <string.h>++static int question(pam_handle_t *pamh, int style, const char *prompt,+                    const char *expected)+{+    const struct pam_conv *conversation;+    struct pam_message message = {style, prompt};+    const struct pam_message *messages[] = {&message};+    struct pam_response *responses = NULL;+    int status = pam_get_item(pamh, PAM_CONV, (const void **)&conversation);+    if (status != PAM_SUCCESS) return status;+    status = conversation->conv(1, messages, &responses, conversation->appdata_ptr);+    if (status == PAM_SUCCESS) {+        status = responses && responses[0].resp &&+            strcmp(responses[0].resp, expected) == 0 ? PAM_SUCCESS : PAM_AUTH_ERR;+    }+    if (responses) {+        free(responses[0].resp);+        free(responses);+    }+    return status;+}++PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags,+                                  int argc, const char **argv)+{+    (void)flags;+    int status = question(pamh, PAM_PROMPT_ECHO_OFF, "Fixture password:",+                          "stage7-test-password");+    if (status == PAM_SUCCESS && argc > 0 && strcmp(argv[0], "two-factor") == 0)+        status = question(pamh, PAM_PROMPT_ECHO_ON, "Fixture code:", "246810");+    return status;+}++PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags,+                             int argc, const char **argv)+{+    (void)pamh; (void)flags; (void)argc; (void)argv;+    return PAM_SUCCESS;+}
+ validation/auth_fixture/serve.py view
@@ -0,0 +1,139 @@+#!/usr/bin/env python3+"""Owned PAM/Kerberos/MariaDB services in a disposable Linux container.++Nothing in this module is intended to run on a developer's host. Generated+keys, ticket caches, test PAM policies and users exist only in this fixture.+"""+import json+import os+from pathlib import Path+import signal+import subprocess+import time++ROOT = Path(os.environ.get('MB_AUTH_FIXTURE_ROOT', '/fixture'))+PORT = int(os.environ.get('MB_AUTH_FIXTURE_PORT', '3306'))+REALM = 'MARIADB-HASKELL.TEST'+SERVICE = f'mariadb/localhost@{REALM}'+++def command(args, **kwargs):+    return subprocess.run(list(map(str, args)), check=True, **kwargs)+++def main():+    ROOT.mkdir(mode=0o700, exist_ok=True)+    os.environ.update(KRB5_CONFIG=str(ROOT/'krb5.conf'),+                      KRB5_KDC_PROFILE=str(ROOT/'kdc.conf'))+    (ROOT/'krb5.conf').write_text(f'''[libdefaults]+ default_realm = {REALM}+ dns_lookup_kdc = false+ dns_lookup_realm = false+ rdns = false+ udp_preference_limit = 1+[realms]+ {REALM} = {{+  kdc = 127.0.0.1:10088+ }}+''')+    (ROOT/'kdc.conf').write_text(f'''[kdcdefaults]+ kdc_ports = 10088+ kdc_tcp_ports = 10088+[realms]+ {REALM} = {{+  database_name = {ROOT}/principal+  key_stash_file = {ROOT}/stash+  acl_file = {ROOT}/kadm5.acl+  max_life = 1d+ }}+''')+    (ROOT/'kadm5.acl').write_text('')+    command(['kdb5_util', 'create', '-s', '-P', 'stage7-disposable-master'])+    for principal in [SERVICE, f'driver@{REALM}', f'other@{REALM}']:+        command(['kadmin.local', '-q', f'addprinc -randkey {principal}'])+    command(['kadmin.local', '-q', f'ktadd -k {ROOT}/server.keytab {SERVICE}'])+    for name in ['driver', 'other']:+        command(['kadmin.local', '-q', f'ktadd -k {ROOT}/{name}.keytab {name}@{REALM}'])+    for service, argument in [('single', ''), ('multi', ' two-factor')]:+        Path(f'/etc/pam.d/mariadb-haskell-{service}').write_text(+            'auth required /opt/driver-auth/pam_fixture.so'+argument+'\n'+            'account required pam_permit.so\n')+    command(['openssl', 'req', '-x509', '-newkey', 'rsa:2048', '-nodes',+             '-keyout', ROOT/'tls-key.pem', '-out', ROOT/'tls-cert.pem',+             '-days', '7', '-subj', '/CN=localhost',+             '-addext', 'subjectAltName=DNS:localhost,IP:127.0.0.1'],+            stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)+    command(['mariadb-install-db', '--no-defaults', f'--datadir={ROOT}/data',+             '--auth-root-authentication-method=normal', '--skip-test-db'],+            stdout=subprocess.DEVNULL)+    processes = []+    logs = []+    def stop(*_):+        raise KeyboardInterrupt+    signal.signal(signal.SIGTERM, stop)+    signal.signal(signal.SIGINT, stop)+    try:+        for name, args in [('kdc', ['krb5kdc', '-n']), ('server', [+            'mariadbd', '--no-defaults', '--user=root', f'--datadir={ROOT}/data',+            f'--socket={ROOT}/server.sock', f'--pid-file={ROOT}/server.pid',+            '--bind-address=0.0.0.0', f'--port={PORT}',+            f'--ssl-cert={ROOT}/tls-cert.pem', f'--ssl-key={ROOT}/tls-key.pem',+            '--plugin-load-add=auth_pam', '--plugin-load-add=auth_gssapi',+            f'--gssapi-principal-name={SERVICE}', f'--gssapi-keytab-path={ROOT}/server.keytab',+            '--character-set-server=utf8mb4', '--collation-server=utf8mb4_unicode_ci'])]:+            log = (ROOT/f'{name}.log').open('w')+            logs.append(log)+            processes.append(subprocess.Popen(args, stdout=log, stderr=subprocess.STDOUT))+        client = ['mariadb', '--no-defaults', f'--socket={ROOT}/server.sock', '-uroot']+        for _ in range(120):+            if any(p.poll() is not None for p in processes):+                raise RuntimeError('Authentication fixture service exited; inspect /fixture/*.log')+            ready = subprocess.run(client+['-e', 'SELECT 1'], capture_output=True)+            if ready.returncode == 0:+                break+            time.sleep(0.25)+        else:+            raise RuntimeError('Authentication fixture startup deadline exceeded')+        command(client, input=f'''+CREATE DATABASE mariadb_haskell_validation;+CREATE USER 'mariadb_pam'@'%' IDENTIFIED VIA pam USING 'mariadb-haskell-single';+CREATE USER 'mariadb_pam_multi'@'%' IDENTIFIED VIA pam USING 'mariadb-haskell-multi';+CREATE USER 'mariadb_gssapi'@'%' IDENTIFIED VIA gssapi AS 'driver@{REALM}';+GRANT ALL ON mariadb_haskell_validation.* TO 'mariadb_pam'@'%','mariadb_pam_multi'@'%','mariadb_gssapi'@'%';+''', text=True)+        # Populate service tickets too: host clients can authenticate without+        # changing host DNS/realm configuration or contacting a different KDC.+        for name in ['driver', 'other']:+            cache = f'FILE:{ROOT}/{name}.ccache'+            command(['kinit', '-c', cache, '-kt', ROOT/f'{name}.keytab', f'{name}@{REALM}'])+            command(['kvno', '-c', cache, SERVICE])+        (ROOT/'ready.json').write_text(json.dumps({'realm': REALM, 'principal': SERVICE,+            'password': 'stage7-test-password', 'code': '246810'}))+        while True:+            if any(p.poll() is not None for p in processes):+                raise RuntimeError('Authentication fixture service stopped')+            time.sleep(1)+    except KeyboardInterrupt:+        pass+    except Exception:+        import sys+        for filename in ['kdc.log', 'server.log']:+            path = ROOT/filename+            if path.is_file():+                print(filename+":\n"+path.read_text()[-12000:], file=sys.stderr)+        raise+    finally:+        for p in reversed(processes):+            p.terminate()+        for p in reversed(processes):+            try:+                p.wait(timeout=10)+            except subprocess.TimeoutExpired:+                p.kill()+                p.wait()+        for log in logs:+            log.close()+++if __name__ == '__main__':+    main()
+ validation/cdc_fixture.py view
@@ -0,0 +1,109 @@+#!/usr/bin/env python3+"""Run one CDC check on a disposable, socket-only semisynchronous MariaDB.++The managed server and its settings are untouched. The child has a deadline,+cleanup always waits for the owned server, and no network listener is exposed.+Only the temporary datadir is removed; existing server data is never opened.+"""+import os+from pathlib import Path+import shutil+import subprocess+import sys+import tempfile+import time+++def main(arguments):+    if not arguments:+        raise ValueError('expected validation executable and arguments')+    required = ('mariadbd', 'mariadb-install-db', 'mariadb')+    programs = {name: shutil.which(name) for name in required}+    if not all(programs.values()):+        raise RuntimeError('installed MariaDB server binaries are required for the CDC fixture')+    with tempfile.TemporaryDirectory(prefix='mb-cdc-', dir='/tmp') as name:+        directory = Path(name)+        datadir = directory/'data'+        sock = directory/'server.sock'+        with (directory/'server.log').open('w+') as log:+            subprocess.run([programs['mariadb-install-db'], '--no-defaults', '--datadir='+str(datadir),+                            '--auth-root-authentication-method=normal', '--skip-test-db'],+                           stdout=log, stderr=subprocess.STDOUT, check=True, timeout=45)+            command = [programs['mariadbd'], '--no-defaults', '--datadir='+str(datadir),+                       '--socket='+str(sock), '--pid-file='+str(directory/'server.pid'),+                       '--skip-networking', '--server-id=610129', '--log-bin='+str(directory/'bin'),+                       '--binlog-format=ROW', '--binlog-row-image=FULL', '--binlog-checksum=CRC32',+                       '--plugin-load-add=semisync_master', '--rpl-semi-sync-master-timeout=5000',+                       '--default-time-zone=+00:00']+            if os.geteuid() == 0:+                command.append('--user=root')+            server = subprocess.Popen(command, stdout=log, stderr=subprocess.STDOUT)+            child = None+            try:+                def sql(statement):+                    return subprocess.run([programs['mariadb'], '--no-defaults', '--protocol=socket',+                        '--socket='+str(sock), '--user=root', '--batch', '--skip-column-names'],+                        input=statement, text=True, capture_output=True, timeout=8)+                deadline = time.monotonic()+30+                while time.monotonic() < deadline:+                    ready = sql('SELECT VERSION()')+                    if ready.returncode == 0:+                        break+                    if server.poll() is not None:+                        raise RuntimeError('disposable CDC server stopped before readiness')+                    time.sleep(0.1)+                else:+                    raise RuntimeError('disposable CDC server readiness deadline')+                setup = sql('CREATE DATABASE cdc_validation; '+                            'CREATE TABLE cdc_validation.changes(id INT PRIMARY KEY); '+                            'SET GLOBAL rpl_semi_sync_master_enabled=1;')+                if setup.returncode:+                    raise RuntimeError('semisync fixture setup failed: '+setup.stderr)+                print('Disposable CDC MariaDB:', ready.stdout.strip(), flush=True)+                child = subprocess.Popen(arguments, env={**os.environ, 'MB_CDC_SOCKET': str(sock),+                                                        'MB_CDC_CONTROL': str(directory)})+                child_deadline = time.monotonic()+40+                while child.poll() is None:+                    if time.monotonic() >= child_deadline:+                        raise RuntimeError('CDC fixture child deadline exceeded')+                    request = directory/'restart.request'+                    if request.exists():+                        request.unlink()+                        server.terminate()+                        server.wait(timeout=10)+                        server = subprocess.Popen(command, stdout=log, stderr=subprocess.STDOUT)+                        ready_deadline = time.monotonic()+10+                        while time.monotonic() < ready_deadline:+                            if sql('SELECT 1').returncode == 0:+                                break+                            if server.poll() is not None:+                                raise RuntimeError('CDC fixture failed to restart')+                            time.sleep(0.05)+                        else:+                            raise RuntimeError('CDC restart readiness deadline')+                        (directory/'restart.done').touch()+                    time.sleep(0.02)+                return child.returncode+            except BaseException:+                log.flush()+                log.seek(0)+                print(log.read()[-4000:], file=sys.stderr)+                raise+            finally:+                if child is not None and child.poll() is None:+                    child.terminate()+                    try:+                        child.wait(timeout=5)+                    except subprocess.TimeoutExpired:+                        child.kill()+                        child.wait(timeout=5)+                server.terminate()+                try:+                    server.wait(timeout=10)+                except subprocess.TimeoutExpired:+                    server.kill()+                    server.wait(timeout=10)+++if __name__ == '__main__':+    raise SystemExit(main(sys.argv[1:]))
+ validation/check.py view
@@ -0,0 +1,377 @@+#!/usr/bin/env python3+"""Verify MariaDB, rebuild current source, then validate with run.py.++Rebuilding prevents a stale executable from being measured after a source edit.+"""+import argparse+import json+import os+from pathlib import Path+import subprocess+import sys++import release_recorder as run+import publish_results as live+import stage1_gates+++def unchanged_inputs(expected):+    """Do not measure a binary built while source/test inputs were changing."""+    current = live.input_snapshot()+    return all(current.get(key) == expected.get(key) for key in ('files', 'commit', 'branch'))+++def install_runtime_data():+    """Exercise Cabal installation, rather than making CA lookup depend on cwd.++    Reuse the successful v2 build's Setup configuration. Cabal copies the data+    declared by this package, including the CA snapshot, to its configured+    installation prefix; it does not register or resolve another library build.+    """+    binary = Path(run.executable("exe:mariadb-validation"))+    build = next((parent / "opt" for parent in binary.parents+                  if (parent / "opt/setup-config").is_file()+                  and (parent / "opt/build/Database/MySQL/Connection.hi").is_file()), None)+    if build is None:+        raise RuntimeError("library Setup configuration not found for runtime data installation")+    return subprocess.run(["cabal", "v1-copy", "lib:mariadb-haskell", "--builddir=" + str(build)],+                          cwd=run.ROOT).returncode+++def native_benchmarks(output, repetitions, config):+    """Keep additive native measurements in every complete validation run."""+    destination = output / "native"+    status = subprocess.call([sys.executable, str(run.HERE / "stage3_benchmarks.py"),+                              "--config", str(config), "--output", str(destination),+                              "--repetitions", str(repetitions)], cwd=run.ROOT)+    if status:+        raise RuntimeError("native benchmark recorder failed; its partial evidence is retained")+    data = json.loads((destination / "results.json").read_text())+    return data+++def session_reuse_benchmarks(output, repetitions, config):+    """Require complete request-boundary measurements in every Stage 4 run."""+    destination = output / 'reuse'+    subprocess.run([sys.executable, str(run.HERE / 'stage4_benchmarks.py'),+                    '--config', str(config), '--output', str(destination),+                    '--repetitions', str(repetitions)], cwd=run.ROOT, check=True)+    return json.loads((destination / 'results.json').read_text())+++def cdc_benchmarks(output, repetitions, config):+    """Preserve complete CDC measurements alongside every full Stage 5 run."""+    destination = output / 'cdc'+    subprocess.run([sys.executable, str(run.HERE / 'stage5_benchmarks.py'),+                    '--config', str(config), '--output', str(destination),+                    '--repetitions', str(repetitions)], cwd=run.ROOT, check=True)+    return json.loads((destination / 'results.json').read_text())+++def wire_benchmarks(output, repetitions, config):+    """Keep all optional-wire alternatives in complete Stage 6 runs."""+    destination = output / 'wire'+    subprocess.run([sys.executable, str(run.HERE / 'stage6_benchmarks.py'),+                    '--config', str(config), '--output', str(destination),+                    '--repetitions', str(repetitions)], cwd=run.ROOT, check=True)+    return json.loads((destination / 'results.json').read_text())+++def external_authentication_benchmarks(output, repetitions, config):+    path = output / 'external-authentication'+    subprocess.run([sys.executable, str(run.HERE / 'stage7_benchmarks.py'),+        '--output', str(path), '--repetitions', str(repetitions),+        '--config', str(config)], cwd=run.ROOT, check=True)+    return json.loads((path / 'results.json').read_text())+++def parse_arguments(argv=None):+    """Share option parsing between execution and setup-failure publication."""+    parser=argparse.ArgumentParser(add_help=False)+    parser.add_argument("--config",type=Path,default=run.HERE/".local/environment.json")+    parser.add_argument("--output",type=Path)+    parser.add_argument("--only")+    parser.add_argument("--no-upstream",action="store_true")+    parser.add_argument("--no-benchmarks",action="store_true")+    parser.add_argument("--repetitions",type=int,default=7)+    parser.add_argument("--compare",type=Path)+    parser.add_argument("--freeze",type=Path)+    parser.add_argument("--working-tree",action="store_true")+    args,_=parser.parse_known_args(argv)+    return args+++def is_full_run(args):+    return not (args.only or args.no_upstream or args.no_benchmarks) and args.repetitions >= 7+++# Keep fixture routing independent of non-live wire probes. Prefix/substring+# selections use the recorder's same substring semantics; complete selections+# always need the real services. Live Stage 7 cases follow this inventory.+AUTHENTICATION_FIXTURE_CASES = (+    'auth.pam-environment', 'auth.gssapi-environment',+    'stage7.live.gssapi-rejects-password-only-success',+    'stage7.live.pam-wrong-password', 'stage7.live.pam-multiple-questions',+    'stage7.live.pam-wrong-second-factor', 'stage7.live.pam-cancellation-and-reconnect',+    'stage7.live.gssapi-tls', 'stage7.live.gssapi-premature-ok-rejected',+    'stage7.live.gssapi-wrong-identity', 'stage7.live.gssapi-missing-cache',+    'stage7.live.gssapi-repeated-context-cleanup', 'stage7.live.gssapi-cancel-before-server-token',+)+++def needs_authentication_fixture(args):+    return not args.only or any(args.only in ident for ident in AUTHENTICATION_FIXTURE_CASES)+++def main(args=None):+    if "--help" in sys.argv or "-h" in sys.argv:+        status = subprocess.call([sys.executable,str(run.HERE/"release_recorder.py"),"--help"])+        print("\nFull runs execute native, reuse, CDC, wire and authentication benchmarks and update the live report. Partial runs do not replace it.")+        return status+    args = args if args is not None else parse_arguments()+    full = is_full_run(args)+    inputs = live.input_snapshot()+    output = args.output.resolve() if args.output else run.HERE / "results/runs" / run.dt.datetime.now(run.dt.timezone.utc).strftime("%Y%m%dT%H%M%S.%fZ")+    if output.exists():+        raise SystemExit("run output already exists; use a new directory")+    attempt = {"state":"FAILED", "stage":"server verification", "started_at":live.now(),+               "inputs":inputs, "uncommitted_review":args.working_tree, "run_directory":live.relative(output), "exit_code":1}++    def finish(status, reason="", data=None):+        if full:+            attempt.update(finished_at=live.now(),exit_code=status,reason=reason)+            if data is not None:+                attempt.update(state="COMPLETED",stage="complete")+            gate = None+            # The initial immutable capture predates an accepted implementation.+            # Every normal candidate requires that reference; deleting its+            # manifest must not silently disable the acceptance ratchet.+            if data is not None and not args.freeze:+                try:+                    accepted,manifest = stage1_gates.reference()+                    original = json.loads(live.BASELINE.read_text())+                    previous_stage, stage_manifest = stage1_gates.reference(run.HERE / "results/previous-stage.json")+                    gate = stage1_gates.compare(data,accepted,original,previous_stage=previous_stage)+                    gate["previous_stage_reference"] = stage_manifest+                    gate.update(accepted_reference=manifest,tested_input_sha256=inputs['sha256'])+                    (run.HERE / "results/stage1-gates.json").write_text(json.dumps(gate,indent=2)+"\n")+                    if not gate['correctness_accepted'] or not gate['evidence_accepted']:+                        status = status or 1+                except (OSError, ValueError, KeyError) as error:+                    # A damaged/missing reference must leave a visible failed+                    # attempt, rather than crashing before the live publication.+                    # The candidate's raw results remain in its run directory.+                    attempt.update(state="FAILED", stage="acceptance evidence",+                                   reason="Acceptance evidence could not be verified: " + str(error))+                    data, gate, status = None, None, status or 1+            if data is not None and gate and gate['correctness_accepted'] and gate['evidence_accepted']:+                try:+                    native = native_benchmarks(output, args.repetitions, args.config)+                    from stage3_benchmarks import verify_evidence+                    verify_evidence(native, inputs)+                    attempt['native_features'] = native+                    comparison = native.get('baseline_comparison', {})+                    native_timing = comparison.get('timing_investigations', [])+                    native_allocation = comparison.get('allocation_investigations', [])+                    gate['native_timing_investigations'] = native_timing+                    gate['native_allocation_investigations'] = native_allocation+                    gate['performance_requires_confirmation'] = gate.get('performance_requires_confirmation', False) or bool(native_timing or native_allocation)+                    (run.HERE / 'results/stage1-gates.json').write_text(json.dumps(gate, indent=2) + '\n')+                    if any(item['percent'] > 20 for item in native_timing):+                        status = status or 1+                except (OSError, ValueError, KeyError, RuntimeError) as error:+                    attempt.update(state="FAILED", stage="native benchmarks",+                                   reason="Native benchmark evidence failed: " + str(error))+                    data, gate, status = None, None, status or 1+                except KeyboardInterrupt:+                    attempt.update(state="FAILED", stage="native benchmarks", reason="Native benchmarks interrupted.")+                    data, gate, status = None, None, 130+            if data is not None and any(case['id'].startswith('stage4.') for case in data.get('correctness', [])):+                try:+                    from stage4_benchmarks import verify_evidence as verify_reuse+                    reuse = session_reuse_benchmarks(output, args.repetitions, args.config)+                    verify_reuse(reuse, inputs)+                    attempt['session_reuse'] = reuse+                    comparison = reuse['baseline_comparison']+                    gate['reuse_timing_investigations'] = comparison['timing_investigations']+                    gate['reuse_allocation_investigations'] = comparison['allocation_investigations']+                    gate['performance_requires_confirmation'] = gate.get('performance_requires_confirmation', False) or bool(comparison['timing_investigations'] or comparison['allocation_investigations'])+                    (run.HERE / 'results/stage1-gates.json').write_text(json.dumps(gate, indent=2) + '\n')+                    if any(item['percent'] > 20 for item in comparison['timing_investigations']):+                        status = status or 1+                except (OSError, ValueError, KeyError, RuntimeError, subprocess.CalledProcessError) as error:+                    attempt.update(state='FAILED', stage='session reuse benchmarks',+                                   reason='Session reuse evidence failed: ' + str(error))+                    data, gate, status = None, None, status or 1+                except KeyboardInterrupt:+                    attempt.update(state='FAILED', stage='session reuse benchmarks', reason='Session reuse benchmarks interrupted.')+                    data, gate, status = None, None, 130+            if data is not None and any(case['id'].startswith('stage5.') for case in data.get('correctness', [])):+                try:+                    from stage5_benchmarks import verify_evidence as verify_cdc+                    cdc = cdc_benchmarks(output, args.repetitions, args.config)+                    verify_cdc(cdc, inputs)+                    attempt['cdc'] = cdc+                    comparison = cdc['baseline_comparison']+                    gate['cdc_timing_investigations'] = comparison['timing_investigations']+                    gate['cdc_allocation_investigations'] = comparison['allocation_investigations']+                    gate['performance_requires_confirmation'] = gate.get('performance_requires_confirmation', False) or bool(comparison['timing_investigations'] or comparison['allocation_investigations'])+                    (run.HERE / 'results/stage1-gates.json').write_text(json.dumps(gate, indent=2) + '\n')+                    if any(item['percent'] > 20 for item in comparison['timing_investigations']):+                        status = status or 1+                except (OSError, ValueError, KeyError, RuntimeError, subprocess.CalledProcessError) as error:+                    attempt.update(state='FAILED', stage='CDC benchmarks', reason='CDC evidence failed: '+str(error))+                    data, gate, status = None, None, status or 1+                except KeyboardInterrupt:+                    attempt.update(state='FAILED', stage='CDC benchmarks', reason='CDC benchmarks interrupted.')+                    data, gate, status = None, None, 130+            if data is not None and any(case['id'].startswith('stage6.') for case in data.get('correctness', [])):+                try:+                    from stage6_benchmarks import verify_evidence as verify_wire+                    wire = wire_benchmarks(output, args.repetitions, args.config)+                    verify_wire(wire, inputs)+                    attempt['wire_features'] = wire+                    comparison = wire['baseline_comparison']+                    for metric in ('timing_investigations', 'allocation_investigations'):+                        gate['wire_' + metric] = comparison[metric]+                    gate['performance_requires_confirmation'] = gate.get('performance_requires_confirmation', False) or bool(+                        comparison['timing_investigations'] or comparison['allocation_investigations'])+                    (run.HERE / 'results/stage1-gates.json').write_text(json.dumps(gate, indent=2) + '\n')+                    if any(item['percent'] > 20 for item in comparison['timing_investigations']):+                        status = status or 1+                except (OSError, ValueError, KeyError, RuntimeError, subprocess.CalledProcessError) as error:+                    attempt.update(state='FAILED', stage='wire feature benchmarks', reason='Wire feature evidence failed: ' + str(error))+                    data, gate, status = None, None, status or 1+                except KeyboardInterrupt:+                    attempt.update(state='FAILED', stage='wire feature benchmarks', reason='Wire feature benchmarks interrupted.')+                    data, gate, status = None, None, 130+            if data is not None and any(case['id'].startswith('stage7.') for case in data.get('correctness', [])):+                try:+                    from stage7_benchmarks import verify_evidence as verify_auth+                    auth = external_authentication_benchmarks(output, args.repetitions, args.config)+                    verify_auth(auth, inputs)+                    attempt['external_authentication'] = auth+                    comparison = auth['baseline_comparison']+                    for metric in ('timing_investigations', 'allocation_investigations'):+                        gate['authentication_' + metric] = comparison[metric]+                    gate['performance_requires_confirmation'] = gate.get('performance_requires_confirmation', False) or bool(+                        comparison['timing_investigations'] or comparison['allocation_investigations'])+                    (run.HERE / 'results/stage1-gates.json').write_text(json.dumps(gate, indent=2) + '\n')+                    if any(item['percent'] > 20 for item in comparison['timing_investigations']):+                        status = status or 1+                except (OSError, ValueError, KeyError, RuntimeError, subprocess.CalledProcessError) as error:+                    attempt.update(state='FAILED', stage='authentication benchmarks', reason='Authentication evidence failed: ' + str(error))+                    data, gate, status = None, None, status or 1+                except KeyboardInterrupt:+                    attempt.update(state='FAILED', stage='authentication benchmarks', reason='Authentication benchmarks interrupted.')+                    data, gate, status = None, None, 130+            attempt.update(exit_code=status, finished_at=live.now())+            live.publish(attempt,data,gate=gate)+            print("Live results updated:",live.REPORT,flush=True)+        else:+            print("Partial/short development run: live full-suite results were not replaced.",flush=True)+        return status++    try:+        env=os.environ.copy(); env.update(json.loads(args.config.read_text()))+        version=run.sql_client(env,"SELECT VERSION()")+        if "MariaDB" not in version: raise RuntimeError("expected MariaDB")+    except Exception as error:+        print("MariaDB must be installed, configured and running before validation. See validation/MACOS.md.\n"+str(error),file=sys.stderr)+        return finish(1,"MariaDB verification failed before build; see terminal output and MACOS.md.")+    print("Server verified before build:",version,flush=True)+    attempt["stage"]="build"+    try:+        build = subprocess.run(["cabal","build","exe:mariadb-validation","test:test","test:integration","--enable-tests","-O2"],cwd=run.ROOT)+    except OSError as error:+        return finish(1, "Build process could not start: " + str(error))+    except KeyboardInterrupt:+        return finish(130, "Validation interrupted during build.")+    if build.returncode:+        return finish(build.returncode,"Current source did not build; no suite results were produced.")+    if not unchanged_inputs(inputs):+        return finish(1, "Test/build inputs changed during build; no suite was measured.")+    attempt["stage"]="runtime data installation"+    try:+        installed = install_runtime_data()+    except (OSError, RuntimeError) as error:+        return finish(1, "Runtime data installation failed: " + str(error))+    except KeyboardInterrupt:+        return finish(130, "Validation interrupted during runtime data installation.")+    if installed:+        return finish(installed, "Runtime data installation failed; no suite results were produced.")+    if not unchanged_inputs(inputs):+        return finish(1, "Test/build inputs changed during runtime data installation; no suite was measured.")+    attempt["stage"]="suite"+    options = [*[arg for arg in sys.argv[1:] if arg != "--working-tree"],"--config",str(args.config),"--output",str(output)]+    if full and not args.compare and not args.freeze:+        options += ["--compare",str(live.BASELINE)]+    try:+        status = subprocess.call([sys.executable,str(run.HERE/"release_recorder.py"),*options],cwd=run.ROOT)+    except OSError as error:+        return finish(1, "Recorder process could not start: " + str(error))+    except KeyboardInterrupt:+        return finish(130, "Validation interrupted during suite execution.")+    final = live.input_snapshot()+    if final["files"] != inputs["files"] or final["commit"] != inputs["commit"]:+        return finish(1,"Code or Git revision changed during build/execution; rerun on stable committed inputs.")+    results = output / "results.json"+    if not results.exists():+        return finish(status or 1,"The recorder stopped without a complete results file; see terminal output and run logs.")+    try:+        data = json.loads(results.read_text()) if full else None+    except (OSError, ValueError) as error:+        return finish(status or 1, "Recorder results could not be read: " + str(error))+    return finish(status,data=data)+++def main_with_authentication():+    """Publish setup failures only for the same full-run modes as execution."""+    if "--help" in sys.argv or "-h" in sys.argv:+        return main()+    args = parse_arguments()+    started = live.now()+    try:+        return _main_with_authentication(args)+    except (OSError, ValueError, RuntimeError, subprocess.SubprocessError, KeyboardInterrupt) as error:+        status = 130 if isinstance(error, KeyboardInterrupt) else 1+        reason = 'Authentication fixture setup interrupted.' if status == 130 else str(error)+        if is_full_run(args):+            snapshot = live.input_snapshot()+            live.publish({'state': 'FAILED', 'stage': 'external authentication fixture preflight',+                'started_at': started, 'finished_at': live.now(), 'exit_code': status,+                'inputs': snapshot, 'uncommitted_review': args.working_tree,+                'run_directory': 'validation/results/runs',+                'reason': 'External authentication fixture could not be prepared: ' + reason})+        print('External authentication fixture preflight failed: ' + reason, file=sys.stderr)+        return status+++def _main_with_authentication(args=None):+    """Keep required services alive through correctness and feature benchmarks.++    Non-live filtered checks do not acquire Docker/Kerberos resources. A private+    config overrides only this invocation; original argv and caller settings are+    preserved, including unknown options forwarded to the frozen recorder.+    """+    args = args if args is not None else parse_arguments()+    if not needs_authentication_fixture(args):+        return main(args)+    settings = json.loads(args.config.read_text())+    env = {**os.environ, **settings}+    if env.get("MB_AUTH_HOST"):+        return main(args)+    # Verify the managed server before acquiring/building any test resources.+    if "MariaDB" not in run.sql_client(env, "SELECT VERSION()"):+        raise RuntimeError("expected a running MariaDB server before validation")+    import auth_fixture+    import tempfile+    with auth_fixture.environment() as fixture:+        with tempfile.NamedTemporaryFile(mode="w", suffix=".json", dir=run.HERE/".local") as config:+            json.dump({**settings, **fixture}, config)+            config.flush()+            configured = argparse.Namespace(**{**vars(args), 'config': Path(config.name)})+            return main(configured)+++if __name__=="__main__": raise SystemExit(main_with_authentication())
+ validation/configure_macos.py view
@@ -0,0 +1,81 @@+#!/usr/bin/env python3+"""Configure an installed Homebrew MariaDB for this local validation suite.++Preserves the data directory and refuses to overwrite unrelated my.cnf files.+Creates a private test CA without adding it to the macOS trust store.+"""+import os+from pathlib import Path+import subprocess+import tempfile+import time++MARKER = "# Local development MariaDB, managed by Homebrew."+++def call(args, **kwargs):+    return subprocess.run(args,check=True,**kwargs)+++def main():+    prefix=Path(subprocess.check_output(["brew","--prefix"],text=True).strip())+    package=Path(subprocess.check_output(["brew","--prefix","mariadb@12.3"],text=True).strip())+    openssl=prefix / "opt/openssl@4/bin/openssl"+    if not openssl.exists(): openssl=Path("openssl")+    config=prefix / "etc/my.cnf"+    if config.exists() and not config.read_text().startswith(MARKER):+        raise SystemExit(f"{config} already contains another configuration. Merge the documented validation settings into it explicitly.")+    tls=prefix / "var/mariadb-haskell/tls"+    tls.mkdir(parents=True,exist_ok=True,mode=0o700)+    def crypto(*args):+        result=subprocess.run([str(openssl),*map(str,args)],capture_output=True,text=True)+        if result.returncode: raise SystemExit(result.stderr)+    if not (tls / "ca.pem").exists():+        crypto("req","-x509","-newkey","rsa:2048","-nodes","-keyout",tls/"ca-key.pem","-out",tls/"ca.pem","-sha256","-days","3650","-subj","/CN=MariaDB Haskell Local Test CA")+    with tempfile.TemporaryDirectory(prefix="mariadb-haskell-tls-") as temporary:+        folder=Path(temporary)+        extension=folder/"tls.cnf"+        extension.write_text("[v3]\nbasicConstraints=CA:FALSE\nkeyUsage=digitalSignature,keyEncipherment\nextendedKeyUsage=serverAuth,clientAuth\nsubjectAltName=DNS:localhost,DNS:Winter,IP:127.0.0.1\n")+        for stem,days in [("server",365),("client",365),("expired-client",0)]:+            cert=tls/(stem+"-cert.pem"); key=tls/(stem+"-key.pem")+            if cert.exists() and key.exists():+                valid=subprocess.run([str(openssl),"x509","-in",str(cert),"-noout","-checkend","86400"],capture_output=True).returncode==0+                if not days or valid: continue+            request=folder/(stem+".csr")+            crypto("req","-new","-newkey","rsa:2048","-nodes","-keyout",key,"-out",request,"-subj","/CN=localhost")+            validity=["-days",str(days)] if days else ["-not_before","20200101000000Z","-not_after","20210101000000Z"]+            crypto("x509","-req","-in",request,"-CA",tls/"ca.pem","-CAkey",tls/"ca-key.pem","-CAcreateserial","-out",cert,*validity,"-sha256","-extfile",extension,"-extensions","v3")+    for path in tls.glob("*.pem"): path.chmod(0o600)+    config.write_text(f"""{MARKER}+[mariadbd]+bind-address=127.0.0.1+port=3306+max-allowed-packet=384M+server-id=610123+log-bin=mariadb-bin+binlog-format=ROW+binlog-row-image=FULL+binlog-checksum=CRC32+character-set-server=utf8mb4+collation-server=utf8mb4_unicode_ci+default-time-zone=+00:00+ssl-ca={tls}/ca.pem+ssl-cert={tls}/server-cert.pem+ssl-key={tls}/server-key.pem+""")+    config.chmod(0o644)+    call(["brew","services","restart","mariadb@12.3"])+    uid=os.getuid()+    subprocess.run(["launchctl","kickstart",f"gui/{uid}/sh.brew.mariadb@12.3"],check=False,capture_output=True)+    client=str(package/"bin/mariadb")+    for _ in range(30):+        result=subprocess.run([client,"--protocol=socket","--connect-timeout=1","--batch","--skip-column-names","-e","SELECT VERSION(), @@bind_address, @@have_ssl, @@log_bin"],capture_output=True,text=True)+        if result.returncode==0:+            print(result.stdout.strip()); break+        time.sleep(1)+    else: raise SystemExit("MariaDB did not become ready: "+result.stderr)+    print("Managed configuration:",config)+    print("Private local TLS files:",tls)+++if __name__=="__main__": main()
+ validation/connector_c.c view
@@ -0,0 +1,135 @@+/* Matching text-protocol control using the installed MariaDB Connector/C.+ * Same SQL, ASCII payload, numeric parsing, checksums, warmups and transactions+ * as Validation.Benchmark. Haskell-only binary/TLS/micro workloads are separate.+ */+#include <mysql.h>+#include <stdio.h>+#include <stdlib.h>+#include <string.h>+#include <inttypes.h>+#include <time.h>+#include <sys/resource.h>++static MYSQL *db;+static const char *name;+static int count;+static uint64_t now_ns(void) {+    struct timespec t;+    clock_gettime(CLOCK_MONOTONIC, &t);+    return (uint64_t)t.tv_sec * 1000000000ULL + t.tv_nsec;+}+static uint64_t cpu_ns(void) {+    struct rusage r;+    getrusage(RUSAGE_SELF, &r);+    return ((uint64_t)r.ru_utime.tv_sec+r.ru_stime.tv_sec)*1000000000ULL+         + ((uint64_t)r.ru_utime.tv_usec+r.ru_stime.tv_usec)*1000ULL;+}+static void die(void) { fprintf(stderr,"Connector/C: %s\n",mysql_error(db)); exit(1); }+static void execute(const char *q) {+    if (mysql_real_query(db,q,strlen(q))) die();+    int next;+    do {+        MYSQL_RES *result=mysql_store_result(db);+        if (result) mysql_free_result(result);+        else if (mysql_field_count(db)) die();+        next=mysql_next_result(db);+        if (next>0) die();+    } while (next==0);+}+static uint64_t fetch(const char *q) {+    if (mysql_real_query(db,q,strlen(q))) die();+    MYSQL_RES *result=mysql_use_result(db);+    if (!result) die();+    MYSQL_FIELD *fields=mysql_fetch_fields(result);+    unsigned n=mysql_num_fields(result);+    MYSQL_ROW row;+    uint64_t checksum=0;+    while ((row=mysql_fetch_row(result))) {+        unsigned long *lengths=mysql_fetch_lengths(result);+        for (unsigned i=0;i<n;i++) if (row[i]) {+            switch (fields[i].type) {+                case MYSQL_TYPE_TINY: case MYSQL_TYPE_SHORT:+                case MYSQL_TYPE_LONG: case MYSQL_TYPE_LONGLONG:+                case MYSQL_TYPE_INT24: case MYSQL_TYPE_DECIMAL:+                case MYSQL_TYPE_NEWDECIMAL:+                    checksum+=(uint64_t)strtoll(row[i],NULL,10); break;+                default: checksum+=lengths[i]; break;+            }+        }+    }+    if (mysql_errno(db)) die();+    mysql_free_result(result);+    return checksum;+}+static uint64_t action(void) {+    if (!strcmp(name,"latency.text")) return fetch("SELECT 1");+    if (!strcmp(name,"read.text.narrow")) return fetch("SELECT id,n,payload FROM mb_bench_data ORDER BY id");+    if (!strcmp(name,"read.text.wide")) {+        char q[2048]="SELECT id,n";+        for (int i=0;i<30;i++) strcat(q,",payload");+        strcat(q," FROM mb_bench_data ORDER BY id");+        return fetch(q);+    }+    if (!strcmp(name,"blob.text")) return fetch("SELECT CAST(REPEAT('x',1048576) AS BINARY)");+    execute("TRUNCATE TABLE mb_bench_write");+    execute("BEGIN");+    if (!strcmp(name,"write.execute-many")) {+        size_t capacity=(size_t)count*80+1;+        char *q=calloc(capacity,1);+        if (!q) exit(2);+        size_t offset=0;+        for (int i=1;i<=count;i++) offset+=(size_t)snprintf(q+offset,capacity-offset,+            "%sINSERT INTO mb_bench_write(n) VALUES(%d)",i==1?"":";",i);+        execute(q); free(q);+    } else {+        char q[128];+        for (int i=1;i<=count;i++) {+            snprintf(q,sizeof(q),"INSERT INTO mb_bench_write(n) VALUES(%d)",i);+            execute(q);+        }+    }+    execute("COMMIT");+    if (fetch("SELECT COUNT(*) FROM mb_bench_write")!=(uint64_t)count) {+        fprintf(stderr,"inserted row count mismatch\n"); exit(1);+    }+    return fetch("SELECT COALESCE(SUM(n),0) FROM mb_bench_write");+}+int main(int argc,char **argv) {+    if (argc==2 && !strcmp(argv[1],"--version")) { puts(mysql_get_client_info()); return 0; }+    if (argc!=3) { fprintf(stderr,"usage: connector-c BENCH ITERATIONS\n"); return 2; }+    name=argv[1]; count=atoi(argv[2]);+    if (count<=0) return 2;+    const char *host=getenv("MB_HOST"),*user=getenv("MB_USER"),*pass=getenv("MB_PASSWORD"),*database=getenv("MB_DATABASE");+    db=mysql_init(NULL);+    unsigned timeout=5;+    mysql_options(db,MYSQL_OPT_CONNECT_TIMEOUT,&timeout);+    mysql_options(db,MYSQL_SET_CHARSET_NAME,"utf8mb4");+    if (!mysql_real_connect(db,host?host:"127.0.0.1",user,pass,database,+        getenv("MB_PORT")?(unsigned)atoi(getenv("MB_PORT")):3306,NULL,CLIENT_MULTI_STATEMENTS)) die();+    if (mysql_get_ssl_cipher(db)) { fprintf(stderr,"unexpected TLS in plaintext control\n"); return 1; }+    int iterations=count,units=1;+    uint64_t expected=1;+    if (!strcmp(name,"read.text.narrow")) { units=10000; expected=201300000; }+    else if (!strcmp(name,"read.text.wide")) { units=10000; expected=238420000; }+    else if (!strcmp(name,"blob.text")) { units=1048576; expected=1048576; }+    else if (!strcmp(name,"write.text") || !strcmp(name,"write.execute-many")) {+        units=count; iterations=1; expected=(uint64_t)count*(count+1)/2;+        execute("CREATE TEMPORARY TABLE mb_bench_write(n BIGINT) ENGINE=InnoDB");+    } else if (strcmp(name,"latency.text")) { fprintf(stderr,"unsupported control workload\n"); return 3; }+    for (int i=0;i<2;i++) if (action()!=expected) { fprintf(stderr,"warm-up checksum mismatch\n"); return 1; }+    uint64_t *samples=calloc((size_t)iterations,sizeof(uint64_t));+    if (!samples) return 2;+    uint64_t checksum=0,cpu0=cpu_ns(),start=now_ns();+    for (int i=0;i<iterations;i++) {+        uint64_t t=now_ns(),v=action();+        samples[i]=now_ns()-t;+        if (v!=expected) { fprintf(stderr,"benchmark checksum mismatch\n"); return 1; }+        checksum+=v;+    }+    uint64_t elapsed=now_ns()-start,cpu=cpu_ns()-cpu0;+    printf("BENCH\t%s\t%d\t%" PRIu64 "\t-1\t-1\t-1\t%" PRIu64 "\t%" PRIu64 "\t",name,iterations*units,elapsed,cpu,checksum);+    for (int i=0;i<iterations;i++) printf("%s%" PRIu64,i?",":"",samples[i]);+    putchar('\n'); free(samples);+    mysql_close(db);+    return 0;+}
+ validation/feature_benchmarks.py view
@@ -0,0 +1,288 @@+#!/usr/bin/env python3+"""Shared recorder and independent evidence checks for additive feature suites.++Workload-specific units/checksums live in immutable specifications. Both native+and session-reuse recorders use these same completeness and baseline guards.+"""+from dataclasses import dataclass, field+import argparse+import json+import hashlib+import os+import platform+from pathlib import Path+import random+import statistics++import publish_results as live+import release_recorder as run++@dataclass(frozen=True)+class FeatureSpec:+    """Wire record identity, workloads and independent expected work."""+    marker: str+    command: str+    label: str+    profiles: dict+    units: dict+    checksums: dict+    pairs: dict+    baseline_directory: Path+    workload_paths: tuple+    heap_profiles: dict = field(default_factory=dict)+    environment_profile: tuple = ()+    environment_profile_aliases: dict = field(default_factory=dict)+++def sample(spec, output, ident):+    """Require one complete record, positive work and the independent checksum."""+    lines = [line for line in output.splitlines() if line.startswith(spec.marker + '\t')]+    if len(lines) != 1:+        raise ValueError(f'expected exactly one {spec.label} benchmark record')+    marker, name, *values = lines[0].split('\t')+    if marker != spec.marker or name != ident or len(values) != 6:+        raise ValueError(f'{spec.label} benchmark record shape/identity mismatch')+    result = dict(zip(('units', 'elapsed_ns', 'allocated_bytes', 'cpu_ns', 'gc_cpu_ns', 'checksum'), map(int, values)))+    if (result['units'] <= 0 or result['elapsed_ns'] <= 0 or result['allocated_bytes'] <= 0+            or any(value < 0 for value in result.values())):+        raise ValueError(f'invalid {spec.label} benchmark counters')+    expected = spec.checksums[ident]+    if result['checksum'] != expected:+        raise ValueError(f'{spec.label} benchmark checksum mismatch')+    expected_units = spec.units[ident]+    if result['units'] != expected_units:+        raise ValueError(f'{spec.label} benchmark unit count mismatch')+    return result+++def verify_heap(spec, ident, entries):+    """Require measured heap at every checkpoint in both warmups and the run.++    A fixed 2 MiB growth allowance covers RTS bookkeeping without allowing a+    million-row history to masquerade as bounded streaming. These are live+    post-major-GC bytes, separately from process high-water memory.+    """+    checkpoints = list(spec.heap_profiles[ident])+    if (not isinstance(entries, list) or len(entries) != 3*len(checkpoints)+            or any(not isinstance(entry, list) or len(entry) != 2+                   or any(type(value) is not int for value in entry) or entry[1] <= 0 for entry in entries)+            or [entry[0] for entry in entries] != checkpoints*3):+        raise ValueError('streaming retained heap checkpoints are incomplete')+    width = len(checkpoints)+    for offset in range(0, len(entries), width):+        window = entries[offset:offset+width]+        if any(entry[1] > window[0][1]+2*1024*1024 for entry in window):+            raise ValueError('streaming retained heap grows beyond its streaming allowance')+++def verify_measurements(spec, data, inputs):+    """Validate samples/units independently of any baseline comparison."""+    if (not data.get('complete') or data.get('repetitions', 0) < 7+            or data.get('inputs', {}).get('sha256') != inputs['sha256']):+        raise ValueError(f'{spec.label} benchmark evidence is incomplete or stale')+    profile = data.get('environment_profile', {})+    if spec.environment_profile and (set(profile) != set(spec.environment_profile)+            or not all(isinstance(value, str) and value for value in profile.values())):+        raise ValueError(f'{spec.label} environment profile is missing or incomplete')+    if data['inputs'].get('files') != inputs.get('files'):+        raise ValueError(f'{spec.label} input file evidence disagrees with current inputs')+    if inputs.get('commit') is not None and (data['inputs'].get('commit') != inputs['commit']+            or data.get('build', {}).get('git_commit') != inputs['commit']):+        raise ValueError(f'{spec.label} tested commit disagrees with current inputs')+    if set(data.get('profiles', {})) != set(spec.profiles):+        raise ValueError(f'{spec.label} benchmark profiles are incomplete')+    keys = ('units', 'elapsed_ns', 'allocated_bytes', 'cpu_ns', 'gc_cpu_ns', 'checksum')+    for ident, profile in data['profiles'].items():+        if len(profile.get('samples', [])) != data['repetitions']:+            raise ValueError(f'{spec.label} benchmark repetitions are incomplete')+        for entry in profile['samples']:+            sample(spec, spec.marker + '\t' + ident + '\t' + '\t'.join(str(entry[k]) for k in keys), ident)+            if ident in spec.heap_profiles:+                verify_heap(spec, ident, entry.get('retained_heap', []))+        for key in keys[1:-1]:+            expected = statistics.median(s[key] / s['units'] for s in profile['samples'])+            if profile.get('median_' + key + '_per_unit') != expected:+                raise ValueError(f'{spec.label} benchmark summary disagrees with samples')++    if set(data.get('comparisons', {})) != set(spec.pairs):+        raise ValueError(f'{spec.label} comparison profiles are incomplete')+    for name, (candidate, reference) in spec.pairs.items():+        pair = data['comparisons'][name]+        if pair.get('current') != candidate or pair.get('baseline') != reference:+            raise ValueError(f'{spec.label} comparison identity mismatch')+        for key in ('elapsed_ns', 'allocated_bytes'):+            expected = 100 * (data['profiles'][candidate]['median_' + key + '_per_unit'] /+                              data['profiles'][reference]['median_' + key + '_per_unit'] - 1)+            if pair.get('change_percent', {}).get(key) != expected:+                raise ValueError(f'{spec.label} comparison disagrees with samples')+++def load_baseline(spec):+    """Require the immutable capture, including both checksummed artifacts.++    The baseline is versioned with this runner. A missing file is a damaged+    validation checkout, rather than permission to skip regression detection.+    Publication repeats this check so deleting/changing it after a run cannot+    leave an apparently valid report pointing to absent or different evidence.+    """+    directory = run.HERE / spec.baseline_directory+    try:+        entries = {}+        for line in (directory / 'SHA256SUMS').read_text().splitlines():+            parts = line.split()+            if len(parts) != 2:+                raise ValueError(f'invalid {spec.label} baseline checksum manifest')+            digest, name = parts+            if name in entries:+                raise ValueError(f'duplicate {spec.label} baseline checksum entry')+            entries[name] = digest+        contents = {name: (directory / name).read_bytes() for name in ('results.json', 'SUMMARY.md')}+    except OSError as error:+        raise ValueError(f'required {spec.label} baseline is missing or unreadable') from error+    if set(entries) != set(contents):+        raise ValueError(f'{spec.label} baseline checksum manifest is incomplete')+    for name, raw in contents.items():+        if hashlib.sha256(raw).hexdigest() != entries[name]:+            raise ValueError(f'{spec.label} baseline checksum mismatch: ' + name)+    baseline = json.loads(contents['results.json'])+    verify_measurements(spec, baseline, baseline['inputs'])+    return baseline, entries['results.json']+++def baseline_comparison(spec, data):+    """Recalculate all deltas and alarms from the verified immutable capture."""+    baseline, expected_hash = load_baseline(spec)+    # json round-trips tuples as lists. Compare dependency identities in a+    # canonical form, both before saving a run and during later publication.+    same_dependencies = sorted(map(tuple, data['dependencies'])) == sorted(map(tuple, baseline['dependencies']))+    def environment_identity(document):+        # Alias only explicitly verified historical identifiers. Recorded+        # measurements/metadata stay unchanged; unknown identities never match.+        return {key: spec.environment_profile_aliases.get(key, {}).get(value, value)+                for key, value in document.get('environment_profile', {}).items()}+    same = (same_dependencies+            and environment_identity(data) == environment_identity(baseline)+            and all(data[key] == baseline[key] for key in ('server', 'machine', 'haskell_benchmark_rts'))+            and all(data['build'][key] == baseline['build'][key] for key in ('ghc', 'cabal'))+            and all(data['inputs']['files'][key] == baseline['inputs']['files'][key] for key in spec.workload_paths))+    comparison = {'reference': live.relative(run.HERE / spec.baseline_directory / 'results.json'),+                  'sha256': expected_hash, 'comparable': same,+                  'timing_investigations': [], 'allocation_investigations': [], 'profiles': {}}+    if same:+        for ident, profile in data['profiles'].items():+            old = baseline['profiles'][ident]+            changes = {key: 100 * (profile['median_' + key + '_per_unit'] / old['median_' + key + '_per_unit'] - 1)+                       for key in ('elapsed_ns', 'allocated_bytes')}+            comparison['profiles'][ident] = changes+            for metric, category in [('elapsed_ns', 'timing_investigations'), ('allocated_bytes', 'allocation_investigations')]:+                if changes[metric] > 5:+                    comparison[category].append({'id': ident, 'percent': changes[metric]})+    return comparison+++def attach_baseline_comparison(spec, data):+    """Attach the required baseline comparison after all measurements complete."""+    data['baseline_comparison'] = baseline_comparison(spec, data)+++def verify_evidence(spec, data, inputs):+    """Require current measurements and independently verified baseline deltas."""+    verify_measurements(spec, data, inputs)+    if data.get('baseline_comparison') != baseline_comparison(spec, data):+        raise ValueError(f'{spec.label} baseline comparison is missing or disagrees with verified evidence')+++def main(spec):+    parser = argparse.ArgumentParser(description=__doc__)+    parser.add_argument('--output', type=Path, required=True)+    parser.add_argument('--config', type=Path, default=run.HERE / '.local/environment.json')+    parser.add_argument('--repetitions', type=int, default=7)+    parser.add_argument('--freeze', action='store_true', help='capture this suite’s first immutable baseline; refuses replacement')+    args = parser.parse_args()+    if args.repetitions < 7:+        parser.error('native confirmation requires at least seven process repetitions')+    if args.output.exists():+        parser.error('output directory exists; previous evidence must be retained')+    env = os.environ.copy()+    env.update(json.loads(args.config.read_text()))+    env['TZ'] = 'UTC'+    version = run.sql_client(env, 'SELECT VERSION()')+    if 'MariaDB' not in version:+        raise RuntimeError('expected MariaDB before build')+    baseline_directory = run.HERE / spec.baseline_directory+    if args.freeze:+        if baseline_directory.exists():+            raise ValueError('refusing to replace an immutable feature baseline')+    else:+        load_baseline(spec)+    inputs = live.input_snapshot()+    run.command(['cabal', 'build', 'exe:mariadb-validation', '-O2', '--enable-tests'], timeout=300)+    if any(live.input_snapshot()[key] != inputs[key] for key in ('files', 'commit')):+        raise RuntimeError(f'inputs changed during {spec.label} benchmark build')+    binary = run.executable('exe:mariadb-validation')+    args.output.mkdir(parents=True)+    plan = json.loads((run.ROOT / 'dist-newstyle/cache/plan.json').read_text())+    dependencies = sorted({(p['pkg-name'], p['pkg-version']) for p in plan['install-plan'] if p['pkg-name'] != 'mariadb-haskell'})+    data = {'schema': 1, 'recorded_at': live.now(), 'inputs': inputs,+            'server': run.server_metadata(env), 'repetitions': args.repetitions,+            'build': run.snapshot(), 'haskell_benchmark_rts': ['-N4', '-T'],+            'dependencies': dependencies,+            'dependency_plan_sha256': hashlib.sha256(json.dumps(dependencies).encode()).hexdigest(),+            'machine': {'system': platform.system(), 'release': platform.release(),+                        'machine': platform.machine(), 'processor': platform.processor(), 'cpu_count': os.cpu_count()},+            'policy': 'two in-process warmups; shuffled independent processes; same-build alternatives; all samples retained',+            'profiles': {ident: {'samples': []} for ident in spec.profiles}, 'complete': False}+    if spec.environment_profile:+        data['environment_profile'] = {key: env[key] for key in spec.environment_profile}+    destination = args.output / 'results.json'+    try:+        for repetition in range(args.repetitions):+            order = list(spec.profiles)+            random.Random(20261107 + repetition).shuffle(order)+            for ident in order:+                output = run.command([binary, spec.command, ident, str(spec.profiles[ident]),+                                      '+RTS', '-N4', '-T', '-RTS'], env=env, timeout=180, check=False)+                (args.output / f'{ident}.{repetition}.log').write_text(output.stdout + output.stderr)+                if output.returncode:+                    raise RuntimeError(f'{spec.label} benchmark process failed: ' + ident)+                entry = sample(spec, output.stdout, ident)+                if ident in spec.heap_profiles:+                    entry['retained_heap'] = [list(map(int, line.split('\t')[1:]))+                        for line in output.stdout.splitlines() if line.startswith(spec.marker+'_HEAP\t')]+                    verify_heap(spec, ident, entry['retained_heap'])+                data['profiles'][ident]['samples'].append(entry)+                print(spec.marker, repetition + 1, ident, flush=True)+        for profile in data['profiles'].values():+            for key in ('elapsed_ns', 'allocated_bytes', 'cpu_ns', 'gc_cpu_ns'):+                profile['median_' + key + '_per_unit'] = statistics.median(s[key] / s['units'] for s in profile['samples'])+        data['comparisons'] = {}+        for name, (candidate, reference) in spec.pairs.items():+            data['comparisons'][name] = {'current': candidate, 'baseline': reference,+                'change_percent': {key: 100 * (data['profiles'][candidate]['median_' + key + '_per_unit'] /+                    data['profiles'][reference]['median_' + key + '_per_unit'] - 1)+                    for key in ('elapsed_ns', 'allocated_bytes')}}+        if any(live.input_snapshot()[key] != inputs[key] for key in ('files', 'commit')):+            raise RuntimeError(f'inputs changed during {spec.label} benchmark measurement')+        data['complete'] = True+        if args.freeze:+            verify_measurements(spec, data, inputs)+            baseline_directory.mkdir(parents=True, exist_ok=False)+            artifacts = {'results.json': (json.dumps(data, indent=2) + '\n').encode(),+                'SUMMARY.md': (f'# Immutable {spec.label} feature baseline\n\n'+                    f'First complete capture: {data["recorded_at"]}.\n\n'+                    f'Input fingerprint: `{inputs["sha256"]}`.\n\n'+                    f'{args.repetitions} independent process repetitions per profile; '+                    'two warmups per process. All samples and checksums are retained.\n').encode()}+            for name, raw in artifacts.items():+                (baseline_directory / name).write_bytes(raw)+            (baseline_directory / 'SHA256SUMS').write_text(''.join(+                hashlib.sha256(raw).hexdigest() + '  ' + name + '\n' for name, raw in artifacts.items()))+        else:+            attach_baseline_comparison(spec, data)+            verify_evidence(spec, data, inputs)+    except Exception as error:+        data['complete'] = False+        data['error'] = str(error)+        raise+    finally:+        destination.write_text(json.dumps(data, indent=2) + '\n')
+ validation/hs/Main.hs view
@@ -0,0 +1,61 @@+{-# LANGUAGE ScopedTypeVariables #-}+module Main where++import Control.Exception+import Control.Monad+import Data.List (find, intercalate)+import System.Environment+import System.Exit+import System.IO+import Validation.Common+import qualified Validation.Protocol as Protocol+import qualified Validation.Functional as Functional+import qualified Validation.Reliability as Reliability+import qualified Validation.Security as Security+import qualified Validation.CDC as CDC+import qualified Validation.Benchmark as Benchmark+import qualified Validation.Supplementary as Supplementary+import qualified Validation.Stage1 as Stage1+import qualified Validation.Stage2 as Stage2+import qualified Validation.Stage3 as Stage3+import qualified Validation.Stage3Bench as Stage3Bench+import qualified Validation.Stage4 as Stage4+import qualified Validation.Stage4Bench as Stage4Bench+import qualified Validation.Stage5 as Stage5+import qualified Validation.Stage5Review as Stage5Review+import qualified Validation.Stage5Bench as Stage5Bench+import qualified Validation.Stage6 as Stage6+import qualified Validation.Stage6Bench as Stage6Bench+import qualified Validation.Stage6Review as Stage6Review+import qualified Validation.Stage7 as Stage7+import qualified Validation.Stage7Bench as Stage7Bench++allCases :: [Case]+allCases=Protocol.cases ++ Functional.cases ++ Reliability.cases ++ Security.cases ++ CDC.cases ++ Stage1.cases ++ Stage2.cases ++ Stage3.cases ++ Stage4.cases ++ Stage5.cases ++ Stage5Review.cases ++ Stage6.cases ++ Stage6Review.cases ++ Stage7.cases++main :: IO ()+main=do+    hSetBuffering stdout LineBuffering+    args <- getArgs+    run args `catches`+        [Handler (\(Unsupported msg) -> hPutStrLn stderr msg >> exitWith (ExitFailure 3))+        ,Handler (\(Unavailable msg) -> hPutStrLn stderr msg >> exitWith (ExitFailure 4))+        ,Handler (\(e :: SomeException) -> case fromException e of+            Just (code :: ExitCode) -> exitWith code+            Nothing -> hPutStrLn stderr (displayException e) >> exitFailure)]+  where+    run ["list"]=forM_ allCases $ \c -> putStrLn (intercalate "\t" [caseId c,caseArea c])+    run ["list-benchmarks"]=forM_ Benchmark.benchmarks $ \(ident,n) -> putStrLn (ident ++ "\t" ++ show n)+    run ["preflight"]=preflight+    run ["case",ident]=case find ((==ident) . caseId) allCases of+        Nothing -> throwIO (userError ("unknown test " ++ ident))+        Just c -> caseRun c >> putStrLn ("PASS\t" ++ ident)+    run ["stage5-bench",ident,n]=Stage5Bench.run ident (read n)+    run ["stage7-bench",ident,n]=Stage7Bench.run ident (read n)+    run ["stage6-bench",ident,n]=Stage6Bench.run ident (read n)+    run ["stage6-heap-control"]=Stage6Bench.retentionNegativeControl+    run ["stage4-bench",ident,n]=Stage4Bench.run ident (read n)+    run ["stage3-bench",ident,n]=Stage3Bench.run ident (read n)+    run ["stage1-bench",ident,n]=Supplementary.run ident (read n)+    run ["bench",ident,n]=Benchmark.runBenchmark ident (read n)+    run _=throwIO (userError "usage: mariadb-validation list | list-benchmarks | preflight | case ID | bench ID ITERATIONS | stage1-bench ID ITERATIONS | stage3-bench ID ITERATIONS")
+ validation/hs/Validation/Benchmark.hs view
@@ -0,0 +1,166 @@+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE ScopedTypeVariables #-}+module Validation.Benchmark (benchmarks, runBenchmark) where++import Control.Concurrent+import Control.Exception hiding (assert)+import Control.Monad+import Data.Binary.Get+import Data.Binary.Put+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as L+import Data.List (intercalate, isPrefixOf)+import qualified Data.Text as T+import qualified Data.Vector as V+import Database.MySQL.Base+import GHC.Clock+import GHC.Stats+import qualified System.IO.Streams as S+import System.Mem+import Validation.Common++benchmarks :: [(String,Int)]+benchmarks =+    [("latency.text",500),("latency.prepared",500)+    ,("read.text.narrow",5),("read.prepared.narrow",5),("read.vector.narrow",5)+    ,("read.text.wide",3),("read.prepared.wide",3)+    ,("read.tls.narrow",5),("read.socket.narrow",5)+    ,("read.parallel-4",3),("read.parallel-16",3)+    ,("write.text",1000),("write.prepared",1000),("write.execute-many",1000)+    ,("blob.text",5),("blob.prepared",5)+    ,("micro.text-row",100000),("micro.binary-row",100000)+    ,("native.bulk",1000),("native.metadata-cache",500)]++narrow,wide :: String+narrow="SELECT id,n,payload FROM mb_bench_data ORDER BY id"+wide="SELECT id,n," ++ intercalate "," (replicate 30 "payload") ++ " FROM mb_bench_data ORDER BY id"++weight :: MySQLValue -> Integer+weight (MySQLText x)=toInteger (T.length x)+weight (MySQLBytes x)=toInteger (B.length x)+weight MySQLNull=0+weight x=number x++consume :: S.InputStream [MySQLValue] -> IO Integer+consume=S.fold (\ !acc row -> acc + sum (map weight row)) 0++consumeVector :: S.InputStream (V.Vector MySQLValue) -> IO Integer+consumeVector=S.fold (\ !acc row -> acc + V.foldl' (\ !s x -> s+weight x) 0 row) 0++textRead :: MySQLConn -> String -> IO Integer+textRead c q=query_ c (sql q) >>= consume . snd++preparedRead :: MySQLConn -> StmtID -> IO Integer+preparedRead c st=queryStmt c st [] >>= consume . snd++parallel :: [IO Integer] -> IO Integer+parallel actions=do+    results <- forM actions $ \action -> do+        done <- newEmptyMVar+        _ <- forkIO (try action >>= putMVar done)+        pure done+    values <- mapM takeMVar results+    sum <$> mapM (either throwIO pure) (values :: [Either SomeException Integer])++-- Each sample is one complete operation (query, transaction, or decoder batch).+-- SQL setup and warm-up happen before counters/timers start.+measure :: String -> Int -> Int -> Integer -> IO Integer -> IO ()+measure name iterations units expected action=do+    replicateM_ 2 (action >>= equal "warm-up checksum" expected)+    performGC+    before <- getRTSStats+    start <- getMonotonicTimeNSec+    samples <- replicateM iterations $ do+        t0 <- getMonotonicTimeNSec+        !checksum <- action+        t1 <- getMonotonicTimeNSec+        equal "benchmark checksum" expected checksum+        pure (t1-t0,checksum)+    end <- getMonotonicTimeNSec+    timed <- getRTSStats+    performGC+    after <- getRTSStats+    putStrLn $ intercalate "\t"+        ["BENCH",name,show (iterations*units),show (end-start)+        ,show (allocated_bytes after-allocated_bytes before)+        ,show (gc_cpu_ns timed-gc_cpu_ns before)+        ,show (max_live_bytes after),show (cpu_ns timed-cpu_ns before)+        ,show (sum (map snd samples)),intercalate "," (map (show . fst) samples)]++runBenchmark :: String -> Int -> IO ()+runBenchmark name n+    | name=="native.bulk" = throwIO (Unsupported "binary bulk API absent")+    | name=="native.metadata-cache" = throwIO (Unsupported "metadata caching absent")+    | name=="micro.text-row" = micro False n+    | name=="micro.binary-row" = micro True n+    | "read.parallel-" `isPrefixOf` name = do+        let workers=read (drop (length ("read.parallel-" :: String)) name)+        ci <- config+        bracket (replicateM workers (connect ci)) (mapM_ quietClose) $ \cs ->+            measure name n (workers*10000) (toInteger workers*201300000) (parallel [textRead c narrow | c <- cs])+    | otherwise=withConnect open $ \c -> case name of+        "latency.text" -> measure name n 1 1 (textRead c "SELECT 1")+        "latency.prepared" -> withStmt c "SELECT 1" $ \st -> measure name n 1 1 (preparedRead c st)+        "read.text.narrow" -> measure name n 10000 201300000 (textRead c narrow)+        "read.prepared.narrow" -> withStmt c narrow $ \st -> measure name n 10000 201300000 (preparedRead c st)+        "read.vector.narrow" -> measure name n 10000 201300000 (queryVector_ c (sql narrow) >>= consumeVector . snd)+        "read.text.wide" -> measure name n 10000 238420000 (textRead c wide)+        "read.prepared.wide" -> withStmt c wide $ \st -> measure name n 10000 238420000 (preparedRead c st)+        "read.tls.narrow" -> measure name n 10000 201300000 (textRead c narrow)+        "read.socket.narrow" -> measure name n 10000 201300000 (textRead c narrow)+        "blob.text" -> measure name n 1048576 1048576 (textRead c "SELECT CAST(REPEAT('x',1048576) AS BINARY)")+        "blob.prepared" -> withStmt c "SELECT CAST(REPEAT('x',1048576) AS BINARY)" $ \st -> measure name n 1048576 1048576 (preparedRead c st)+        "write.text" -> writeBench c Nothing+        "write.prepared" -> do+            exec c "CREATE TEMPORARY TABLE mb_bench_write(n BIGINT) ENGINE=InnoDB"+            withStmt c "INSERT INTO mb_bench_write(n) VALUES(?)" (writeBench c . Just)+        "write.execute-many" -> writeBench c Nothing+        _ -> throwIO (userError ("unknown benchmark " ++ name))+  where+    open=do+        ci <- config+        case name of+            "read.tls.narrow" -> tlsConnect ci "localhost"+            "read.socket.narrow" -> socketPath >>= \p -> connectUnixSocket p ci+            _ -> connect ci+    withStmt c q=bracket (prepareStmt c (sql q)) (closeStmt c)+    writeBench c stmt=do+        exec c "CREATE TEMPORARY TABLE IF NOT EXISTS mb_bench_write(n BIGINT) ENGINE=InnoDB"+        let action=do+                exec c "TRUNCATE TABLE mb_bench_write"+                exec c "BEGIN"+                case (name,stmt) of+                    ("write.execute-many",_) -> void (executeMany c "INSERT INTO mb_bench_write(n) VALUES(?)" [[MySQLInt64 (fromIntegral i)] | i <- [1..n]])+                    (_,Just st) -> forM_ [1..n] $ \i -> void (executeStmt c st [MySQLInt64 (fromIntegral i)])+                    _ -> forM_ [1..n] $ \i -> exec c ("INSERT INTO mb_bench_write(n) VALUES(" ++ show i ++ ")")+                exec c "COMMIT"+                count <- scalarInt c "SELECT COUNT(*) FROM mb_bench_write"+                equal "inserted benchmark row count" (toInteger n) count+                scalarInt c "SELECT COALESCE(SUM(n),0) FROM mb_bench_write"+        measure name 1 n (toInteger n*(toInteger n+1) `div` 2) action++{-# NOINLINE decodeWeight #-}+decodeWeight :: Bool -> [ColumnDef] -> L.ByteString -> IO Integer+decodeWeight binary cols input = case runGetOrFail parser input of+    Left (_,_,err) -> throwIO (userError err)+    Right (_,_,xs) -> evaluate (sum (map weight xs))+  where parser=if binary then getBinaryRow cols (length cols) else getTextRow cols++micro :: Bool -> Int -> IO ()+micro binary n=do+    let name=if binary then "micro.binary-row" else "micro.text-row"+        cols=[ColumnDef "" "" "" "id" "" 63 20 mySQLTypeLongLong 0 0+             ,ColumnDef "" "" "" "n" "" 63 20 mySQLTypeLongLong 0 0+             ,ColumnDef "" "" "" "payload" "" 224 128 mySQLTypeVarString 0 0]+        inputs=[runPut $ if binary+                then putWord8 0 >> putWord8 0 >> putInt64le i >> putInt64le (i*3) >> putLenEncBytes (B.replicate 128 120)+                else putLenEncBytes (fromStringBytes (show i)) >> putLenEncBytes (fromStringBytes (show (i*3))) >> putLenEncBytes (B.replicate 128 120)+               | i <- [1..16]]+        action=go 0 0+          where+            go i !acc+                | i>=n = pure acc+                | otherwise = decodeWeight binary cols (inputs !! (i `mod` 16)) >>= \v -> go (i+1) (acc+v)+        expected=sum [toInteger (4*(i `mod` 16+1)+128) | i <- [0..n-1]]+    measure name 1 n expected action+  where fromStringBytes=B.pack . map (fromIntegral . fromEnum)
+ validation/hs/Validation/CDC.hs view
@@ -0,0 +1,92 @@+{-# LANGUAGE ScopedTypeVariables #-}+module Validation.CDC (cases) where++import Control.Concurrent+import Control.Exception hiding (assert)+import Control.Monad+import Data.Binary.Put+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as L+import Data.IORef+import Data.Word+import Database.MySQL.Base+import Database.MySQL.BinLog+import qualified System.IO.Streams as S+import Validation.Common+import qualified Validation.Stage5 as Stage5++format :: FormatDescription+format=FormatDescription 4 "12.3.3-MariaDB" 0 (B.replicate 171 8)++make :: BinLogEventType -> L.ByteString -> BinLogPacket+make typ body=BinLogPacket 0 typ 1 (fromIntegral (19+L.length body)) 4 0 body False++tableMap :: Word64 -> BinLogPacket+tableMap tid=make BINLOG_TABLE_MAP_EVENT $ runPut $ do+    putWord48le tid; putWord16le 0+    putWord8 1; putByteString "d"; putWord8 0+    putWord8 1; putByteString "t"; putWord8 0+    putLenEncInt 1; putWord8 3; putLenEncInt 0; putWord8 0++writeRow :: Word64 -> Int -> BinLogPacket+writeRow tid n=make BINLOG_WRITE_ROWS_EVENTv1 $ runPut $ do+    putWord48le tid; putWord16le 0; putLenEncInt 1; putWord8 1; putWord8 0; putWord32le (fromIntegral n)++decodeEvents :: [BinLogPacket] -> IO [RowBinLogEvent]+decodeEvents packets=do+    ref <- newIORef "mariadb-bin.000001"+    input <- S.fromList packets+    decodeRowBinLogEvent (format,ref,input) >>= S.toList++cases :: [Case]+cases =+    [ Case "cdc.single-table-map" "cdc" $ do+        actual <- decodeEvents [tableMap 1,writeRow 1 7]+        equal "single row event" 1 (length actual)+    , Case "cdc.multiple-table-maps" "cdc" $ do+        actual <- decodeEvents [tableMap 1,tableMap 2,writeRow 1 7,writeRow 2 8]+        equal "both mapped row events" 2 (length actual)+    , Case "cdc.repeated-row-events" "cdc" $ do+        actual <- decodeEvents [tableMap 1,writeRow 1 7,writeRow 1 8]+        equal "map reused for both row events" 2 (length actual)+    , Case "cdc.table-id-mismatch" "cdc" $ do+        result <- try (decodeEvents [tableMap 1,writeRow 2 7]) :: IO (Either SomeException [RowBinLogEvent])+        case result of+            Left _ -> pure ()+            Right events -> forM_ events $ \e -> case e of+                RowWriteEvent _ _ tm wr -> equal "row matched correct table ID" (tmTableId tm) (writeTableId wr)+                _ -> pure ()+    , Case "cdc.crc32-corruption-rejected" "cdc" $ do+        let body=runPut $ do+                putWord8 0; putWord32le 0; putWord8 16; putWord32le 1+                putWord32le 31; putWord32le 4; putWord16le 0+                putWord64le 7; putWord32le 0xdeadbeef+        expectRejection (getFromPacket (getBinLogPacket True False) (Packet (L.length body) 1 body))+    , Case "cdc.live-row-insert" "cdc" $ withConn $ \source -> do+        exec source "DROP TABLE IF EXISTS mb_cdc_live"+        exec source "CREATE TABLE mb_cdc_live(id INT PRIMARY KEY,n INT) ENGINE=InnoDB"+        tracker <- getLastBinLogTracker source >>= maybe (throwIO (Unavailable "binary log position unavailable")) pure+        stream <- dumpBinLog source 610124 tracker False >>= decodeRowBinLogEvent+        _ <- forkIO $ do+            threadDelay 50000+            withConn (\writer -> exec writer "INSERT INTO mb_cdc_live VALUES(1,7)")+        event <- S.read stream+        case event of+            Just (RowWriteEvent _ _ _ row) -> equal "live insert image" [[BinLogLong 1,BinLogLong 7]] (writeRowData row)+            _ -> throwIO (userError ("missing live row event: " ++ show event))+    , Case "cdc.gtid-resume" "cdc" $ Stage5.gtidResumeFeature+    , Case "cdc.durable-ack-control" "cdc" $ Stage5.manualAckFeature+    ] +++    [ Case ("cdc.native-event-" ++ show typ) "cdc" $ do+        let body=runPut $ do+                putWord8 0; putWord32le 0; putWord8 typ; putWord32le 1+                putWord32le 19; putWord32le 4; putWord16le 0+        result <- try $ do+            p <- getFromPacket (getBinLogPacket False False) (Packet (L.length body) 1 body)+            evaluate (length (show (blEventType p)))+        case result of+            Left e | Just (_ :: DecodePacketException) <- fromException e -> pure ()+            Left (e :: SomeException) -> throwIO (userError ("partial event decoder: " ++ displayException e))+            Right _ -> pure ()+    | typ <- [160..171]+    ]
+ validation/hs/Validation/Common.hs view
@@ -0,0 +1,143 @@+{-# LANGUAGE ScopedTypeVariables #-}+module Validation.Common where++import Control.Exception hiding (assert)+import Control.Monad+import qualified Data.ByteString.Char8 as BC+import Data.List (isInfixOf)+import Data.String (fromString)+import qualified Data.Text as T+import Data.Typeable+import Database.MySQL.Base+import qualified Database.MySQL.TLS as TLS+import System.Environment+import qualified System.IO.Streams as S+import System.Timeout++data Case = Case { caseId :: String, caseArea :: String, caseRun :: IO () }+data Unavailable = Unavailable String deriving (Show, Typeable)+instance Exception Unavailable+data Unsupported = Unsupported String deriving (Show, Typeable)+instance Exception Unsupported++assert :: Bool -> String -> IO ()+assert ok msg = unless ok (throwIO (userError msg))++equal :: (Eq a, Show a) => String -> a -> a -> IO ()+equal label expected actual = assert (expected == actual)+    (label ++ ": expected " ++ take 2000 (show expected) ++ ", got " ++ take 2000 (show actual))++env :: String -> String -> IO String+env name fallback = maybe fallback id <$> lookupEnv name++config :: IO ConnectInfo+config = do+    host <- env "MB_HOST" "127.0.0.1"+    port <- read <$> env "MB_PORT" "3306"+    db <- BC.pack <$> env "MB_DATABASE" "mariadb_haskell_validation"+    user <- BC.pack <$> env "MB_USER" "mariadb_haskell_test"+    pass <- BC.pack <$> env "MB_PASSWORD" ""+    pure defaultConnectInfoMB4 { ciHost=host, ciPort=port, ciDatabase=db, ciUser=user, ciPassword=pass }++socketPath :: IO FilePath+socketPath = env "MB_SOCKET" "/tmp/mysql.sock"++tlsConnect :: ConnectInfo -> String -> IO MySQLConn+tlsConnect ci name = do+    ca <- getEnv "MB_TLS_CA"+    params <- TLS.makeClientParams (TLS.CustomCAStore ca)+    TLS.connect ci (params, name)++quietClose :: MySQLConn -> IO ()+quietClose c = void (timeout 1000000 (close c)) `catch` \(_ :: SomeException) -> pure ()++withConnect :: IO MySQLConn -> (MySQLConn -> IO a) -> IO a+withConnect open = bracket open quietClose++withConn :: (MySQLConn -> IO a) -> IO a+withConn action = config >>= \ci -> withConnect (connect ci) action++withTLS :: (MySQLConn -> IO a) -> IO a+withTLS action = do+    ci <- config+    name <- env "MB_TLS_NAME" "localhost"+    withConnect (tlsConnect ci name) action++sql :: String -> Query+sql = fromString++exec :: MySQLConn -> String -> IO ()+exec c = void . execute_ c . sql++rows :: MySQLConn -> String -> IO [[MySQLValue]]+rows c q = query_ c (sql q) >>= S.toList . snd++preparedRows :: MySQLConn -> String -> [MySQLValue] -> IO [[MySQLValue]]+preparedRows c q ps = bracket (prepareStmt c (sql q)) (closeStmt c) $ \st ->+    queryStmt c st ps >>= S.toList . snd++number :: MySQLValue -> Integer+number v = case v of+    MySQLInt8U x -> fromIntegral x+    MySQLInt8 x -> fromIntegral x+    MySQLInt16U x -> fromIntegral x+    MySQLInt16 x -> fromIntegral x+    MySQLInt32U x -> fromIntegral x+    MySQLInt32 x -> fromIntegral x+    MySQLInt64U x -> fromIntegral x+    MySQLInt64 x -> fromIntegral x+    MySQLDecimal x -> truncate x+    MySQLFloat x -> truncate x+    MySQLDouble x -> truncate x+    _ -> error ("not a numeric value: " ++ show v)++scalar :: MySQLConn -> String -> IO MySQLValue+scalar c q = do+    result <- rows c q+    case result of+        [[x]] -> pure x+        _ -> throwIO (userError ("expected one cell: " ++ take 500 (show result)))++scalarInt :: MySQLConn -> String -> IO Integer+scalarInt c q = number <$> scalar c q++textOf :: MySQLValue -> T.Text+textOf (MySQLText t) = t+textOf (MySQLBytes b) = T.pack (BC.unpack b)+textOf x = error ("not text: " ++ show x)++assertQuery :: String -> String -> [[Integer]] -> Case+assertQuery ident queryText expected = Case ident "sql" $ withConn $ \c -> do+    actual <- map (map number) <$> rows c queryText+    equal queryText expected actual++expectERR :: [Int] -> IO a -> IO ()+expectERR codes action = do+    result <- try action+    case result of+        Left (ERRException e) -> assert (null codes || fromIntegral (errCode e) `elem` codes)+            ("unexpected server error " ++ show e)+        Right _ -> throwIO (userError "expected a server ERR packet")++expectRejection :: IO a -> IO ()+expectRejection action = do+    result <- try action+    case result of+        Left (_ :: SomeException) -> pure ()+        Right _ -> throwIO (userError "operation unexpectedly succeeded")++healthy :: MySQLConn -> IO ()+healthy c = scalarInt c "SELECT 937" >>= equal "following query" 937++preflight :: IO ()+preflight = withConn $ \c -> do+    version <- T.unpack . textOf <$> scalar c "SELECT VERSION()"+    assert ("MariaDB" `isInfixOf` version) ("expected MariaDB, got " ++ version)+    ci <- config+    assert (ciDatabase ci == "mariadb_haskell_validation") "refusing to run outside the reserved validation database"+    maxPacket <- scalarInt c "SELECT @@max_allowed_packet"+    assert (maxPacket >= 300*1024*1024) "set max_allowed_packet >= 300 MiB for the inherited suite"+    equal "row binlog required" "ROW" . textOf =<< scalar c "SELECT @@binlog_format"+    equal "binary logging required" 1 =<< scalarInt c "SELECT @@log_bin"+    equal "server session timezone" "+00:00" . textOf =<< scalar c "SELECT @@time_zone"+    putStrLn ("MariaDB preflight passed: " ++ version)
+ validation/hs/Validation/ExternalAuthFixture.hs view
@@ -0,0 +1,34 @@+-- | Client endpoints for the disposable PAM/Kerberos fixture. Keep these+-- shared benchmark paths independent of changes to correctness case inventories.+module Validation.ExternalAuthFixture (authConfig, pamConnect, gssapiConnect) where++import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as BC+import Database.MySQL.Base+import qualified Database.MySQL.TLS as TLS+import System.Environment++-- The manager supplies only fixture-specific variables. Ordinary validation+-- and benchmark credentials continue to target the managed host MariaDB.+authConfig :: B.ByteString -> IO ConnectInfo+authConfig user = do+    host <- getEnv "MB_AUTH_HOST"+    port <- read <$> getEnv "MB_AUTH_PORT"+    pure defaultConnectInfoMB4 {ciHost=host,ciPort=port,+        ciDatabase="mariadb_haskell_validation",ciUser=user,ciPassword=""}++pamConnect :: B.ByteString -> (DialogPrompt -> IO B.ByteString) -> IO MySQLConn+pamConnect user callback = do+    ci <- authConfig user+    ca <- getEnv "MB_AUTH_TLS_CA"+    params <- TLS.makeClientParams (TLS.CustomCAStore ca)+    TLS.connectWithAuthentication defaultConnectionOptions+        defaultAuthenticationOptions {authenticationDialog=Just callback} ci (params,"localhost")++gssapiConnect :: IO MySQLConn+gssapiConnect = do+    ci <- authConfig "mariadb_gssapi"+    principal <- BC.pack <$> getEnv "MB_AUTH_GSSAPI_PRINCIPAL"+    connectWithAuthentication defaultConnectionOptions+        defaultAuthenticationOptions {authenticationGSSAPIPrincipal=Just principal} ci+
+ validation/hs/Validation/FaultTransport.hs view
@@ -0,0 +1,123 @@+{-# LANGUAGE ScopedTypeVariables #-}+module Validation.FaultTransport where+import Data.Word (Word8)+import Control.Concurrent+import Control.Exception+import Control.Monad+import Data.Bits+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as BC+import qualified Network.Socket as N+import qualified Network.Socket.ByteString as NS+import System.Timeout+import Data.IORef+import qualified System.IO.Streams as S+import Database.MySQL.Base+import qualified Database.MySQL.Connection as C++-- | Deterministic physical packets plus observable writes/finalizer count. This+-- deliberately bypasses authentication while retaining the real framing and+-- ownership guards; live loopback tests below cover actual socket behavior.+scripted :: ConnectionOptions -> [Packet] -> IO (MySQLConn,IO [Packet],IO Int)+scripted options packets = do+    input <- S.fromList packets+    sent <- newIORef []+    closed <- newIORef 0+    conn <- C.newConnection options input (\p -> modifyIORef' sent (p:)) (modifyIORef' closed (+1))+    pure (conn,reverse <$> readIORef sent,readIORef closed)++okPacket :: Word8 -> Packet+okPacket seqN = encodeToPacket seqN (OK 0 0 2 0)+textTranscript :: [Packet]+textTranscript =+    [putToPacket 1 (putLenEncInt 1)+    ,encodeToPacket 2 (ColumnDef "" "" "" "n" "" 63 11 mySQLTypeLong 0 0)+    ,encodeToPacket 3 (EOF 0 2)+    ,putToPacket 4 (putLenEncBytes "7")+    ,encodeToPacket 5 (EOF 0 2)]++-- | Real loopback faults armed by command bytes rather than timing sleeps. A+-- dropped COMMIT reply is read from the backend before disconnecting the client,+-- allowing a separate connection to check durability without replaying COMMIT.+data ProxyMode = FragmentTraffic Int | DropCommitReply | DropTargetReply deriving Show++data ProxyStats = ProxyStats { proxyCommits :: Int, proxyPackets :: Int, proxyCommands :: Int } deriving Show++withFaultProxy :: ConnectInfo -> ProxyMode -> (ConnectInfo -> IO ProxyStats -> IO a) -> IO a+withFaultProxy ci mode action = N.withSocketsDo $ bracket listener N.close $ \server -> do+    case mode of+        FragmentTraffic size | size <= 0 -> fail "proxy fragment size must be positive"+        _ -> pure ()+    N.SockAddrInet port _ <- N.getSocketName server+    stats <- newIORef (ProxyStats 0 0 0)+    finished <- newEmptyMVar+    let stop worker = do+            killThread worker+            joined <- timeout 1000000 (takeMVar finished)+            when (joined==Nothing) (fail "proxy worker did not terminate")+    bracket (forkFinally (serve server stats) (const (putMVar finished ()))) stop $ \_ ->+        action ci {ciHost="127.0.0.1",ciPort=port} (readIORef stats)+  where+    listener = bracketOnError (N.socket N.AF_INET N.Stream N.defaultProtocol) N.close $ \sock -> do+        N.setSocketOption sock N.ReuseAddr 1+        N.bind sock (N.SockAddrInet 0 (N.tupleToHostAddress (127,0,0,1)))+        N.listen sock 1+        pure sock+    serve listenerSocket stats = bracket (fst <$> N.accept listenerSocket) N.close $ \client -> do+        addresses <- N.getAddrInfo (Just N.defaultHints {N.addrSocketType=N.Stream}) (Just (ciHost ci)) (Just (show (ciPort ci)))+        address <- case addresses of+            firstAddress:_ -> pure firstAddress+            [] -> fail "proxy backend address unavailable"+        bracket (N.socket (N.addrFamily address) N.Stream N.defaultProtocol) N.close $ \backend -> do+            N.connect backend (N.addrAddress address)+            armed <- newIORef False+            done <- newEmptyMVar+            let send sock packet = case mode of+                    FragmentTraffic size -> chunks sock size packet+                    _ -> NS.sendAll sock packet+                upstream = forever $ do+                    (header,body) <- receivePacket client+                    let queryBody=if not (B.null body) && B.head body==3 then BC.map toUpperASCII (B.tail body) else ""+                        isCommit=queryBody=="COMMIT"+                        target="MB_STAGE1_FAULT" `B.isInfixOf` queryBody+                    when (not (B.null body) && B.head body `elem` [3,14,22,23,25,26,250])+                        (updateStats stats (\x -> x {proxyCommands=proxyCommands x+1}))+                    when isCommit (updateStats stats (\x -> x {proxyCommits=proxyCommits x+1}))+                    case mode of+                        DropCommitReply -> when isCommit (writeIORef armed True)+                        DropTargetReply -> when target (writeIORef armed True)+                        _ -> pure ()+                    send backend (header<>body)+                downstream = do+                    (header,body) <- receivePacket backend+                    dropReply <- readIORef armed+                    unless dropReply $ do+                        updateStats stats (\x -> x {proxyPackets=proxyPackets x+1})+                        send client (header<>body)+                        downstream+            -- Publish both worker finalizers before allowing cancellation of+            -- the parent. Otherwise cancellation between forks can leak one.+            mask $ \restore -> do+                first <- forkFinally (restore upstream) (const (void (tryPutMVar done ())))+                second <- forkFinally (restore downstream) (const (void (tryPutMVar done ())))+                    `onException` killThread first+                restore (takeMVar done) `finally` (killThread first >> killThread second)+    -- Upstream/downstream update different fields of the same record. Atomic+    -- updates avoid a packet counter overwriting a concurrent command count.+    updateStats ref change = atomicModifyIORef' ref (\stats -> (change stats, ()))+    receivePacket sock = do+        header <- exactly sock 4+        let size=fromIntegral (B.index header 0) .|. (fromIntegral (B.index header 1) `shiftL` 8) .|. (fromIntegral (B.index header 2) `shiftL` 16)+        body <- exactly sock size+        pure (header,body)+    exactly sock size = loop size []+      where+        loop 0 acc = pure (B.concat (reverse acc))+        loop n acc = do+            part <- NS.recv sock n+            when (B.null part) (throwIO (userError "proxy peer closed"))+            loop (n-B.length part) (part:acc)+    chunks _ _ packet | B.null packet = pure ()+    chunks sock size packet = NS.sendAll sock (B.take size packet) >> chunks sock size (B.drop size packet)+    toUpperASCII ch | ch >= 'a' && ch <= 'z' = toEnum (fromEnum ch-32)+                    | otherwise = ch
+ validation/hs/Validation/Functional.hs view
@@ -0,0 +1,399 @@+{-# LANGUAGE ScopedTypeVariables #-}+module Validation.Functional (cases) where++import Control.Exception hiding (assert)+import Control.Monad+import Data.Binary.Put+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as BC+import qualified Data.ByteString.Lazy as L+import Data.List (intercalate)+import Data.Scientific+import qualified Data.Text as T+import Data.Time+import Database.MySQL.Base+import qualified Data.Vector as V+import qualified System.IO.Streams as S+import Validation.Common++data Sample = Sample String String MySQLValue++samples :: [Sample]+samples =+    [ Sample "int8-min" "TINYINT" (MySQLInt8 minBound)+    , Sample "int8-max" "TINYINT" (MySQLInt8 maxBound)+    , Sample "uint8-max" "TINYINT UNSIGNED" (MySQLInt8U maxBound)+    , Sample "int16-min" "SMALLINT" (MySQLInt16 minBound)+    , Sample "int16-max" "SMALLINT" (MySQLInt16 maxBound)+    , Sample "uint16-max" "SMALLINT UNSIGNED" (MySQLInt16U maxBound)+    , Sample "int24-min" "MEDIUMINT" (MySQLInt32 (-8388608))+    , Sample "int24-max" "MEDIUMINT" (MySQLInt32 8388607)+    , Sample "uint24-max" "MEDIUMINT UNSIGNED" (MySQLInt32U 16777215)+    , Sample "int32-min" "INT" (MySQLInt32 minBound)+    , Sample "int32-max" "INT" (MySQLInt32 maxBound)+    , Sample "uint32-max" "INT UNSIGNED" (MySQLInt32U maxBound)+    , Sample "int64-min" "BIGINT" (MySQLInt64 minBound)+    , Sample "int64-max" "BIGINT" (MySQLInt64 maxBound)+    , Sample "uint64-max" "BIGINT UNSIGNED" (MySQLInt64U maxBound)+    , Sample "decimal-65-30" "DECIMAL(65,30)" (MySQLDecimal (scientific (read (replicate 65 '9')) (-30)))+    , Sample "decimal-65-38" "DECIMAL(65,38)" (MySQLDecimal (scientific (negate (read (replicate 65 '9'))) (-38)))+    , Sample "decimal-zero" "DECIMAL(65,30)" (MySQLDecimal 0)+    , Sample "float-exact" "FLOAT" (MySQLFloat 3.125)+    , Sample "double-exact" "DOUBLE" (MySQLDouble (-12345.125))+    , Sample "bit-1" "BIT(1)" (MySQLBit 1)+    , Sample "bit-8" "BIT(8)" (MySQLBit 255)+    , Sample "bit-16" "BIT(16)" (MySQLBit 57514)+    , Sample "bit-64" "BIT(64)" (MySQLBit maxBound)+    , Sample "year-zero" "YEAR" (MySQLYear 0)+    , Sample "year-min" "YEAR" (MySQLYear 1901)+    , Sample "year-max" "YEAR" (MySQLYear 2155)+    , Sample "date-min" "DATE" (MySQLDate (fromGregorian 1000 1 1))+    , Sample "date-leap" "DATE" (MySQLDate (fromGregorian 2000 2 29))+    , Sample "date-max" "DATE" (MySQLDate (fromGregorian 9999 12 31))+    , Sample "datetime-fraction" "DATETIME(6)" (MySQLDateTime (LocalTime (fromGregorian 2024 2 29) (TimeOfDay 12 34 56.123456)))+    , Sample "datetime-max" "DATETIME(6)" (MySQLDateTime (LocalTime (fromGregorian 9999 12 31) (TimeOfDay 23 59 59.999999)))+    , Sample "timestamp-fraction" "TIMESTAMP(6)" (MySQLTimeStamp (LocalTime (fromGregorian 2024 2 29) (TimeOfDay 12 34 56.123456)))+    , Sample "timestamp-after-2038" "TIMESTAMP(6)" (MySQLTimeStamp (LocalTime (fromGregorian 2100 1 1) (TimeOfDay 1 2 3.123456)))+    , Sample "time-zero" "TIME(6)" (MySQLTime 0 (TimeOfDay 0 0 0))+    , Sample "time-positive-limit" "TIME(6)" (MySQLTime 0 (TimeOfDay 838 59 59.999999))+    , Sample "time-negative-limit" "TIME(6)" (MySQLTime 1 (TimeOfDay 838 59 59.999999))+    , Sample "text-empty" "VARCHAR(255)" (MySQLText "")+    , Sample "text-unicode" "VARCHAR(255)" (MySQLText "MariaDB Ελληνικά 中文 العربية")+    , Sample "text-emoji" "VARCHAR(255)" (MySQLText "🦭🚀𐍈")+    , Sample "text-combining" "VARCHAR(255)" (MySQLText "e\x0301")+    , Sample "text-controls" "VARCHAR(255)" (MySQLText "a\0b\n\r\t\\'\"\SUB")+    , Sample "bytes-all-256" "VARBINARY(256)" (MySQLBytes (B.pack [0..255]))+    , Sample "bytes-empty" "VARBINARY(255)" (MySQLBytes B.empty)+    , Sample "null" "VARCHAR(255)" MySQLNull+    , Sample "enum" "ENUM('red','green','blue')" (MySQLText "green")+    , Sample "set" "SET('red','green','blue')" (MySQLText "red,blue")+    ]++roundtrip :: Bool -> Sample -> Case+roundtrip binary (Sample name typ value) = Case+    ("types." ++ mode ++ "." ++ name) "types" $ withConn $ \c -> do+        exec c "SET time_zone='+00:00'"+        exec c ("CREATE TEMPORARY TABLE mb_value(v " ++ typ ++ ")")+        st <- prepareStmt c "INSERT INTO mb_value VALUES (?)"+        void (executeStmt c st [value])+        closeStmt c st+        actual <- if binary then preparedRows c "SELECT v FROM mb_value" [] else rows c "SELECT v FROM mb_value"+        equal (typ ++ " " ++ mode) [[value]] actual+        healthy c+  where mode=if binary then "binary" else "text"++connectionCases :: [Case]+connectionCases =+    [ Case "connection.tcp-native" "connection" $ withConn $ \c -> void (ping c) >> healthy c+    , Case "connection.unix-socket" "connection" $ do+        ci <- config; path <- socketPath+        withConnect (connectUnixSocket path ci) healthy+    , Case "connection.tls-verified" "connection" $ withTLS $ \c -> do+        rs <- rows c "SHOW SESSION STATUS LIKE 'Ssl_cipher'"+        case rs of+            [[_,v]] -> assert (not (T.null (textOf v))) "TLS cipher is empty"+            _ -> throwIO (userError "missing TLS cipher")+        healthy c+    , Case "connection.tls-wrong-hostname" "connection" $ do+        ci <- config+        expectRejection (withConnect (tlsConnect ci "wrong.invalid") healthy)+    , Case "connection.wrong-password" "connection" $ do+        ci <- config+        expectERR [1045] (withConnect (connect ci {ciPassword="definitely-wrong"}) healthy)+    , Case "connection.unknown-user" "connection" $ do+        ci <- config+        expectERR [1045] (withConnect (connect ci {ciUser="mb_user_does_not_exist"}) healthy)+    , Case "connection.missing-database" "connection" $ do+        ci <- config+        expectERR [1044,1049] (withConnect (connect ci {ciDatabase="mb_database_does_not_exist"}) healthy)+    , Case "connection.default-full-unicode" "connection" $ do+        ci <- config+        withConnect (connect ci {ciCharset=ciCharset defaultConnectInfo}) $ \c -> do+            actual <- textOf <$> scalar c "SELECT @@character_set_connection"+            equal "default wire charset" "utf8mb4" actual+    ] +++    [ Case ("auth." ++ plugin ++ "." ++ transport) "authentication" $ do+        ci <- config+        let authci=ci {ciUser=BC.pack user}+        open <- case transport of+            "tls" -> pure (tlsConnect authci "localhost")+            "socket" -> do path <- socketPath; pure (connectUnixSocket path authci)+            _ -> pure (connect authci)+        withConnect open healthy+    | (plugin,user) <- [("ed25519","mariadb_ed25519"),("parsec","mariadb_parsec"),("sha2","mariadb_sha2")]+    , transport <- ["tcp","tls","socket"]+    ]++sqlCases :: [Case]+sqlCases =+    [ assertQuery "sql.cte" "WITH x AS (SELECT 7 AS n) SELECT n+1 FROM x" [[8]]+    , assertQuery "sql.recursive-cte" "WITH RECURSIVE x(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM x WHERE n<10) SELECT SUM(n) FROM x" [[55]]+    , assertQuery "sql.window" "SELECT ROW_NUMBER() OVER (ORDER BY seq) FROM seq_1_to_3" [[1],[2],[3]]+    , assertQuery "sql.json-functions" "SELECT JSON_VALID('{\"n\":7}'), JSON_VALUE('{\"n\":7}', '$.n')+0" [[1,7]]+    , assertQuery "sql.sequence-table" "SELECT COUNT(*), MIN(seq), MAX(seq) FROM seq_1_to_100" [[100,1,100]]+    , Case "sql.sequence-object" "sql" $ withConn $ \c -> do+        exec c "DROP SEQUENCE IF EXISTS mb_validation_seq"+        exec c "CREATE SEQUENCE mb_validation_seq START WITH 41"+        equal "next sequence values" [[41,42]] . map (map number) =<< rows c "SELECT NEXT VALUE FOR mb_validation_seq, NEXT VALUE FOR mb_validation_seq"+        exec c "DROP SEQUENCE mb_validation_seq"+    , Case "sql.generated-column" "sql" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_generated(n INT, doubled INT AS (n*2) PERSISTENT)"+        exec c "INSERT INTO mb_generated(n) VALUES (21)"+        equal "generated value" 42 =<< scalarInt c "SELECT doubled FROM mb_generated"+    , Case "sql.check-constraint" "sql" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_check(n INT CHECK (n>0))"+        expectERR [4025] (exec c "INSERT INTO mb_check VALUES (-1)")+        healthy c+    , Case "sql.foreign-key" "sql" $ withConn $ \c -> do+        exec c "DROP TABLE IF EXISTS mb_child, mb_parent"+        exec c "CREATE TABLE mb_parent(id INT PRIMARY KEY) ENGINE=InnoDB"+        exec c "CREATE TABLE mb_child(id INT PRIMARY KEY, pid INT, FOREIGN KEY(pid) REFERENCES mb_parent(id)) ENGINE=InnoDB"+        expectERR [1452] (exec c "INSERT INTO mb_child VALUES(1,999)")+        exec c "INSERT INTO mb_parent VALUES(7)"+        exec c "INSERT INTO mb_child VALUES(1,7)"+        equal "foreign key row" 7 =<< scalarInt c "SELECT pid FROM mb_child"+        exec c "DROP TABLE mb_child, mb_parent"+    , Case "sql.insert-returning" "sql" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_returning(id INT)"+        equal "insert returning" [[7],[8]] . map (map number) =<< rows c "INSERT INTO mb_returning VALUES(7),(8) RETURNING id"+        equal "delete returning" [[7]] . map (map number) =<< rows c "DELETE FROM mb_returning WHERE id=7 RETURNING id"+        healthy c+    , Case "sql.prepared-returning" "sql" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_returning(id INT)"+        equal "prepared returning" [[9]] . map (map number) =<< preparedRows c "INSERT INTO mb_returning VALUES(?) RETURNING id" [MySQLInt32 9]+        healthy c+    , Case "sql.procedure-multiple-results" "sql" $ withConn $ \c -> do+        exec c "DROP PROCEDURE IF EXISTS mb_results"+        exec c "CREATE PROCEDURE mb_results() BEGIN SELECT 7; SELECT 8; END"+        actual <- queryMulti_ c "CALL mb_results()"+        equal "row sets and trailing OK" [Right [[7]],Right [[8]],Left 0] (map shape actual)+        healthy c+        exec c "DROP PROCEDURE mb_results"+    , Case "sql.procedure-prepared-out" "sql" $ withConn $ \c -> do+        exec c "DROP PROCEDURE IF EXISTS mb_out"+        exec c "CREATE PROCEDURE mb_out(IN a INT, OUT b INT) SET b=a+1"+        actual <- preparedRows c "CALL mb_out(?,?)" [MySQLInt32 41,MySQLNull]+        equal "prepared OUT result" [[42]] (map (map number) actual)+        exec c "DROP PROCEDURE mb_out"+    , Case "sql.oracle-mode" "sql" $ withConn $ \c -> do+        exec c "SET sql_mode='ORACLE'"+        equal "Oracle functions" [[7,8]] . map (map number) =<< rows c "SELECT NVL(NULL,7), DECODE(1,1,8,0) FROM DUAL"+    , Case "sql.system-versioning" "sql" $ withConn $ \c -> do+        exec c "DROP TABLE IF EXISTS mb_versioned"+        exec c "CREATE TABLE mb_versioned(id INT PRIMARY KEY, n INT) WITH SYSTEM VERSIONING"+        exec c "INSERT INTO mb_versioned VALUES(1,7)"+        exec c "UPDATE mb_versioned SET n=8 WHERE id=1"+        equal "historical versions" [[7],[8]] . map (map number) =<< rows c "SELECT n FROM mb_versioned FOR SYSTEM_TIME ALL ORDER BY n"+        exec c "DROP TABLE mb_versioned"+    , Case "sql.json-native-column" "types" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_json(v JSON)"+        st <- prepareStmt c "INSERT INTO mb_json VALUES(?)"+        void (executeStmt c st [MySQLText "{\"name\":\"🦭\",\"n\":7}"])+        closeStmt c st+        document <- textOf <$> scalar c "SELECT v FROM mb_json"+        assert (T.isInfixOf "🦭" document) "JSON Unicode payload changed"+        equal "JSON extraction" "🦭" . textOf =<< scalar c "SELECT JSON_UNQUOTE(JSON_EXTRACT(v,'$.name')) FROM mb_json"+    , Case "sql.uuid-native-column" "types" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_uuid(v UUID)"+        let uuid="123e4567-e89b-12d3-a456-426614174000"+        st <- prepareStmt c "INSERT INTO mb_uuid VALUES(?)"+        void (executeStmt c st [MySQLText uuid]); closeStmt c st+        equal "UUID text representation" uuid . textOf =<< scalar c "SELECT v FROM mb_uuid"+        actual <- preparedRows c "SELECT v FROM mb_uuid" []+        equal "UUID prepared representation" [[uuid]] (map (map textOf) actual)+    , Case "sql.inet-native-columns" "types" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_inet(v4 INET4, v6 INET6)"+        st <- prepareStmt c "INSERT INTO mb_inet VALUES(?,?)"+        void (executeStmt c st [MySQLText "192.0.2.1",MySQLText "2001:db8::1"]); closeStmt c st+        actual <- preparedRows c "SELECT v4,v6 FROM mb_inet" []+        equal "INET values" [["192.0.2.1","2001:db8::1"]] (map (map textOf) actual)+    , Case "sql.vector-native-column" "types" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_vector(v VECTOR(3))"+        exec c "INSERT INTO mb_vector VALUES(VEC_FromText('[1,2,3]'))"+        let expected=L.toStrict (runPut (mapM_ putFloatle [1,2,3]))+        equal "packed vector bytes" (MySQLBytes expected) =<< scalar c "SELECT v FROM mb_vector"+        equal "vector JSON text" "[1,2,3]" . T.filter (/=' ') . textOf =<< scalar c "SELECT VEC_ToText(v) FROM mb_vector"+    , Case "sql.geometry" "types" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_geom(v GEOMETRY)"+        exec c "INSERT INTO mb_geom VALUES(ST_GeomFromText('POINT(7 8)'))"+        actual <- scalar c "SELECT v FROM mb_geom"+        case actual of+            MySQLGeometry b -> assert (B.length b>=21) "geometry payload truncated"+            _ -> throwIO (userError ("geometry type lost: " ++ show actual))+        equal "geometry coordinates" [[7,8]] . map (map number) =<< rows c "SELECT ST_X(v),ST_Y(v) FROM mb_geom"+    , Case "sql.zero-date-preservation" "types" $ withConn $ \c -> do+        exec c "SET sql_mode=''"+        exec c "CREATE TEMPORARY TABLE mb_zero(v DATE)"+        exec c "INSERT INTO mb_zero VALUES('0000-00-00')"+        value <- scalar c "SELECT v FROM mb_zero"+        st <- prepareStmt c "INSERT INTO mb_zero VALUES(?)"+        void (executeStmt c st [value]); closeStmt c st+        actual <- map (map textOf) <$> rows c "SELECT CAST(v AS CHAR) FROM mb_zero"+        equal "zero date after driver round trip" [["0000-00-00"],["0000-00-00"]] actual+    , Case "sql.unsigned-generated-id" "types" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_id(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY) AUTO_INCREMENT=9223372036854775808"+        ok <- execute_ c "INSERT INTO mb_id VALUES(NULL)"+        equal "generated unsigned ID" (9223372036854775808 :: Integer) (fromIntegral (okLastInsertID ok))+    , Case "parameters.literal-question-mark" "parameters" $ withConn $ \c -> do+        (_,stream) <- query c "SELECT '?', ?" [MySQLInt32 7]+        equal "literal and bound parameter" [[MySQLText "?",MySQLInt32 7]] =<< S.toList stream+    , Case "parameters.comment-question-mark" "parameters" $ withConn $ \c -> do+        (_,stream) <- query c "SELECT ? /* literal ? */" [MySQLInt32 7]+        equal "comment placeholder" [[7]] . map (map number) =<< S.toList stream+    , Case "parameters.no-backslash-text" "parameters" $ withConn $ \c -> do+        exec c "SET sql_mode='NO_BACKSLASH_ESCAPES'"+        (_,stream) <- query c "SELECT ?" [MySQLText "a'b\\c"]+        equal "SQL mode safe text parameter" [[MySQLText "a'b\\c"]] =<< S.toList stream+    , Case "parameters.no-backslash-binary" "parameters" $ withConn $ \c -> do+        exec c "SET sql_mode='NO_BACKSLASH_ESCAPES'"+        equal "SQL mode safe binary parameter" [[MySQLText "a'b\\c"]] =<< preparedRows c "SELECT ?" [MySQLText "a'b\\c"]+    , Case "results.vector-api" "results" $ withConn $ \c -> do+        (_,stream) <- queryVector_ c "SELECT seq FROM seq_1_to_3 ORDER BY seq"+        actual <- map (map number . V.toList) <$> S.toList stream+        equal "vector row stream" [[1],[2],[3]] actual+    , Case "results.prepared-vector-api" "results" $ withConn $ \c -> do+        st <- prepareStmt c "SELECT ? + seq FROM seq_1_to_3 ORDER BY seq"+        (_,stream) <- queryStmtVector c st [MySQLInt32 7]+        equal "prepared vector rows" [[8],[9],[10]] . map (map number . V.toList) =<< S.toList stream+        closeStmt c st+    , Case "results.empty" "results" $ withConn $ \c -> equal "empty result" [] =<< rows c "SELECT 1 WHERE FALSE"+    , Case "results.no-rowset" "results" $ withConn $ \c -> do+        equal "DO has no rows" [] =<< rows c "DO 1"+        healthy c+    , Case "results.multi-statement" "results" $ withConn $ \c -> do+        actual <- queryMulti_ c "DO 1; SELECT 7; SELECT 8"+        equal "all multi results" [Left 0,Right [[7]],Right [[8]]] (map shape actual)+        healthy c+    , Case "results.discard-and-reuse" "results" $ withConn $ \c -> do+        (_,stream) <- query_ c "SELECT seq FROM seq_1_to_1000"+        void (S.read stream); S.skipToEof stream; healthy c+    , Case "errors.syntax-and-reuse" "errors" $ withConn $ \c -> expectERR [1064] (rows c "THIS IS NOT SQL") >> healthy c+    , Case "errors.missing-table-and-reuse" "errors" $ withConn $ \c -> expectERR [1146] (rows c "SELECT * FROM mb_no_such_table") >> healthy c+    , Case "errors.duplicate-and-reuse" "errors" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_unique(id INT PRIMARY KEY)"+        exec c "INSERT INTO mb_unique VALUES(1)"+        expectERR [1062] (exec c "INSERT INTO mb_unique VALUES(1)")+        healthy c+    , Case "statements.reset-consumed" "statements" $ withConn $ \c -> do+        st <- prepareStmt c "SELECT ?"+        void (queryStmt c st [MySQLInt32 7] >>= S.toList . snd)+        resetStmt c st+        equal "reuse after reset" [[8]] . map (map number) =<< (queryStmt c st [MySQLInt32 8] >>= S.toList . snd)+        closeStmt c st+    , Case "statements.parameter-type-change" "statements" $ withConn $ \c -> do+        st <- prepareStmt c "SELECT ?"+        a <- queryStmt c st [MySQLInt32 7] >>= S.toList . snd+        b <- queryStmt c st [MySQLText "🦭"] >>= S.toList . snd+        d <- queryStmt c st [MySQLNull] >>= S.toList . snd+        equal "int parameter" [[7]] (map (map number) a)+        equal "new text type" [[MySQLText "🦭"]] b+        equal "new NULL type" [[MySQLNull]] d+        closeStmt c st+    , Case "statements.ddl-reprepare" "statements" $ withConn $ \c -> do+        exec c "DROP TABLE IF EXISTS mb_reprepare"+        exec c "CREATE TABLE mb_reprepare(a INT)"+        exec c "INSERT INTO mb_reprepare VALUES(7)"+        st <- prepareStmt c "SELECT * FROM mb_reprepare"+        void (queryStmt c st [] >>= S.toList . snd)+        exec c "ALTER TABLE mb_reprepare ADD b INT DEFAULT 8"+        equal "replacement metadata" [[7,8]] . map (map number) =<< (queryStmt c st [] >>= S.toList . snd)+        closeStmt c st+        exec c "DROP TABLE mb_reprepare"+    , Case "statements.prepare-metadata" "statements" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_metadata(id BIGINT UNSIGNED NOT NULL PRIMARY KEY,v DECIMAL(20,6))"+        (prepared,params,cols) <- prepareStmtDetail c "SELECT id,v FROM mb_metadata WHERE id=? AND v>?"+        equal "parameter count" 2 (stmtParamCnt prepared)+        equal "result count" 2 (stmtColumnCnt prepared)+        equal "parameter definitions" 2 (length params)+        equal "column names" ["id","v"] (map columnName cols)+        case cols of+            [idCol,valueCol] -> do+                assert (flagUnsigned (columnFlags idCol)) "unsigned result flag missing"+                equal "decimal scale" 6 (columnDecimals valueCol)+            _ -> throwIO (userError "expected two column definitions")+        closeStmt c (stmtId prepared)+    , Case "statements.wrong-parameter-count" "statements" $ withConn $ \c -> do+        st <- prepareStmt c "SELECT ?+?"+        expectRejection (queryStmt c st [MySQLInt32 1] >>= S.toList . snd)+        healthy c+        closeStmt c st+    , Case "statements.closed-id-rejected" "statements" $ withConn $ \c -> do+        st <- prepareStmt c "SELECT ?"+        closeStmt c st+        expectERR [1243] (queryStmt c st [MySQLInt32 1] >>= S.toList . snd)+        healthy c+    , Case "session.timestamp-timezone" "session" $ withConn $ \c -> do+        exec c "SET time_zone='+00:00'"+        exec c "CREATE TEMPORARY TABLE mb_timezone(ts TIMESTAMP,dt DATETIME)"+        exec c "INSERT INTO mb_timezone VALUES('2024-02-29 12:00:00','2024-02-29 12:00:00')"+        exec c "SET time_zone='+02:00'"+        actual <- preparedRows c "SELECT ts,dt FROM mb_timezone" []+        equal "TIMESTAMP follows session timezone; DATETIME does not"+            [[MySQLTimeStamp (LocalTime (fromGregorian 2024 2 29) (TimeOfDay 14 0 0)),+              MySQLDateTime (LocalTime (fromGregorian 2024 2 29) (TimeOfDay 12 0 0))]] actual+    , Case "session.use-schema" "session" $ withConn $ \c -> do+        void (command c (COM_INIT_DB "mariadb_haskell_validation"))+        equal "current schema" "mariadb_haskell_validation" . textOf =<< scalar c "SELECT DATABASE()"+        healthy c+    , Case "session.warning-count" "session" $ withConn $ \c -> do+        exec c "SET sql_mode=''"+        exec c "CREATE TEMPORARY TABLE mb_warning(v VARCHAR(1))"+        ok <- execute_ c "INSERT INTO mb_warning VALUES('long')"+        assert (okWarningCnt ok>0) "server warning count was lost"+        equal "server truncated according to SQL mode" "l" . textOf =<< scalar c "SELECT v FROM mb_warning"+    , Case "parameters.many-values" "parameters" $ withConn $ \c -> do+        (_,stream) <- query c "SELECT seq FROM seq_1_to_5 WHERE seq IN (?) ORDER BY seq" [Many [MySQLInt32 1,MySQLInt32 3]]+        equal "IN list" [[1],[3]] . map (map number) =<< S.toList stream+    , Case "parameters.empty-many" "parameters" $ withConn $ \c -> do+        (_,stream) <- query c "SELECT seq FROM seq_1_to_5 WHERE seq IN (?)" [Many []]+        equal "empty IN list" [] =<< S.toList stream+    , Case "parameters.sql-injection-value" "parameters" $ withConn $ \c -> do+        let value=MySQLText "x' OR 1=1; SELECT SLEEP(5); -- "+        actual <- preparedRows c "SELECT ?" [value]+        equal "bound SQL remains data" [[value]] actual+    , Case "batch.empty" "batch" $ withConn $ \c -> equal "empty batch" [] =<< executeMany c "DO ?" ([] :: [[MySQLValue]])+    , Case "batch.ordered-errors" "batch" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_batch(id INT PRIMARY KEY)"+        expectERR [1062] (executeMany c "INSERT INTO mb_batch VALUES(?)" [[MySQLInt32 1],[MySQLInt32 1],[MySQLInt32 2]])+        equal "only preceding statement executed" [[1]] . map (map number) =<< rows c "SELECT id FROM mb_batch"+        healthy c+    ] +++    [ Case ("types.binary.null-columns-" ++ show n) "types" $ withConn $ \c -> do+        let ps=take n (cycle [MySQLNull,MySQLInt32 7])+        actual <- preparedRows c ("SELECT " ++ intercalate "," (replicate n "?")) ps+        equal "NULL bitmap across byte boundaries" [ps] actual+    | n <- [1,2,7,8,9,15,16,17,63,64,65]+    ] +++    [ Case ("packets.live-blob-" ++ show n) "large-packets" $ withConn $ \c -> do+        result <- scalar c ("SELECT CAST(REPEAT('x'," ++ show n ++ ") AS BINARY)")+        case result of+            MySQLBytes b -> do+                equal "blob length" n (B.length b)+                assert (B.all (==120) b) "blob bytes changed"+            _ -> throwIO (userError ("blob row missing or misclassified: " ++ take 200 (show result)))+        healthy c+    | n <- [0,1,16383,16384,16385,1048576,16777214,16777215,16777216,16777217,33554430]+    ] +++    [ Case ("packets.prepared-outbound-blob-" ++ show n) "large-packets" $ withConn $ \c -> do+        let payload=B.replicate n 120+        actual <- preparedRows c "SELECT ?" [MySQLBytes payload]+        equal "prepared outbound bytes" [[MySQLBytes payload]] actual+        healthy c+    | n <- [16384,16777214,16777215,16777216,16777217,33554430]+    ] +++    [ Case ("packets.text-outbound-blob-" ++ show n) "large-packets" $ withConn $ \c -> do+        let payload=B.replicate n 120+        (_,stream) <- query c "SELECT 937, CAST(? AS BINARY)" [MySQLBytes payload]+        actual <- S.toList stream+        case actual of+            [[idValue,MySQLBytes blob]] -> equal "sentinel" 937 (number idValue) >> equal "text outbound bytes" payload blob+            _ -> throwIO (userError "text command lost its large payload")+        healthy c+    | n <- [16777214,16777215,16777216,16777217]+    ]+  where+    shape (StatementOK ok)=Left (toInteger (okAffectedRows ok))+    shape (StatementRows _ rs)=Right (map (map number) rs)++cases :: [Case]+cases = connectionCases ++ [roundtrip binary s | binary <- [False,True], s <- samples] ++ sqlCases
+ validation/hs/Validation/Protocol.hs view
@@ -0,0 +1,120 @@+{-# LANGUAGE ScopedTypeVariables #-}+module Validation.Protocol (cases) where++import Control.Exception hiding (assert)+import Control.Monad+import Data.Binary (encode)+import Data.Binary.Get+import Data.Binary.Put+import Data.Bits+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as L+import Database.MySQL.Base+import qualified Database.MySQL.Connection as C+import qualified System.IO.Streams as S+import Validation.Common++mkColumn :: FieldType -> ColumnDef+mkColumn typ = ColumnDef "" "" "" "v" "" 63 100 typ 0 0++packetCase :: String -> IO () -> Case+packetCase name = Case ("protocol." ++ name) "protocol"++cases :: [Case]+cases =+    [ packetCase ("length-" ++ show n) $ equal "length round trip" n+        (runGet getLenEncInt (runPut (putLenEncInt n)))+    | n <- [0,1,250,251,255,256,65535,65536,16777215,16777216,2147483647]+    ] +++    [ packetCase ("packet-size-" ++ show n) $ do+        let p=Packet n 7 (L.replicate n 65)+        equal "packet round trip" p (runGet getPacket (encode p))+    | n <- [0,1,255,256,65535]+    ] +++    [ packetCase ("fragment-split-" ++ show n) $ do+        let body="fragmented-packet"+            encoded=L.toStrict (encode (Packet (L.length body) 1 body))+        input <- S.fromList (filter (not . B.null) [B.take n encoded,B.drop n encoded])+        stream <- C.decodeInputStream input+        equal "fragmented packet" (Packet (L.length body) 1 body) =<< C.readPacket stream+    | n <- [1..20]+    ] +++    [ packetCase "coalesced-packets" $ do+        let a=Packet 1 1 "A"; b=Packet 1 2 "B"+        input <- S.fromList [L.toStrict (encode a <> encode b)]+        stream <- C.decodeInputStream input+        equal "first packet" a =<< C.readPacket stream+        equal "second packet" b =<< C.readPacket stream+    , packetCase "truncated-header" $ do+        input <- S.fromList [B.pack [1,0]]+        stream <- C.decodeInputStream input+        expectRejection (C.readPacket stream)+    , packetCase "truncated-body" $ do+        input <- S.fromList [B.pack [3,0,0,1,65]]+        stream <- C.decodeInputStream input+        expectRejection (C.readPacket stream)+    , packetCase "continuation-sequence-rejected" $ do+        input <- S.fromList [Packet 16777215 1 (L.replicate 16777215 65),Packet 0 99 L.empty]+        expectRejection (C.readPacket input)+    , packetCase "continuation-sequence-wrap" $ do+        input <- S.fromList [Packet 16777215 255 (L.replicate 16777215 65),Packet 1 0 "B"]+        result <- C.readPacket input+        equal "joined payload size" 16777216 (pLen result)+        equal "last payload byte" 66 (L.last (pBody result))+    , packetCase "large-row-is-not-eof" $ do+        let body=runPut (putWord8 0xFE >> putWord64le 16777216 >> putLazyByteString (L.replicate 16777216 65))+        assert (not (isEOF (Packet (L.length body) 1 body))) "valid 16 MiB text field classified as EOF"+    , packetCase "unsigned-last-insert-id" $ do+        let body=runPut (putWord8 0 >> putWord8 0 >> putWord8 0xFE >> putWord64le maxBound >> putWord16le 2 >> putWord16le 0)+        result <- getFromPacket getOK (Packet (L.length body) 1 body)+        equal "unsigned ID" (18446744073709551615 :: Integer) (fromIntegral (okLastInsertID result))+    , packetCase "column-definition-roundtrip" $ do+        let col=mkColumn mySQLTypeLong+        result <- decodeFromPacket (encodeToPacket 1 col)+        equal "column definition" col result+    , packetCase "binary-plugin-challenge" $ do+        let nonce=B.pack ([1..9] ++ [0] ++ [11..32])+            body=runPut $ do+                putWord8 10; putByteString "13.2.0-MariaDB"; putWord8 0+                putWord32le 1; putByteString (B.take 8 nonce); putWord8 0+                putWord16le 0x8200; putWord8 45; putWord16le 2; putWord16le 8+                putWord8 32; putByteString (B.replicate 10 0)+                putByteString (B.drop 8 nonce); putByteString "client_ed25519"; putWord8 0+        greeting <- getFromPacket getGreeting (Packet (L.length body) 0 body)+        equal "binary challenge" nonce (greetingSalt1 greeting <> greetingSalt2 greeting)+        equal "plugin identity" "client_ed25519" (greetingAuthPlugin greeting)+    , packetCase "unknown-plugin-rejected" $+        expectRejection (evaluate (B.length (C.scrambleForPlugin "unknown_mariadb_plugin" "12345678901234567890" "password")))+    , packetCase "native-extension-handshake" $+        assert (not (testBit clientCap 0)) "CLIENT_MYSQL legacy marker is still advertised"+    , packetCase "prepared-out-capability" $+        assert (testBit clientCap 18) "CLIENT_PS_MULTI_RESULTS is absent"+    , packetCase "empty-packet-predicate" $ do+        outcome <- try (evaluate (isEOF (Packet 0 1 L.empty)))+        case outcome of+            Left (_ :: SomeException) -> throwIO (userError "empty packet crashes classifier")+            Right value -> equal "empty is not EOF" False value+    , packetCase "text-field-trailing-data" $ do+        let p=putToPacket 1 (putLenEncBytes "1x")+        expectRejection (getFromPacket (getTextField (mkColumn mySQLTypeLong)) p)+    , packetCase "text-field-integer-overflow" $ do+        let p=putToPacket 1 (putLenEncBytes "256")+        expectRejection (getFromPacket (getTextField (mkColumn mySQLTypeTiny)) p)+    , packetCase "reset-does-not-consume-pending-row" $ do+        input <- S.fromList [Packet 5 4 (L.pack [0,0,1,0,0]), encodeToPacket 5 (EOF 0 2), encodeToPacket 1 (OK 0 0 2 0)]+        conn <- C.newConnection C.defaultConnectionOptions input (const (pure ())) (pure ())+        _ <- C.claimCommand conn+        _ <- C.markStreaming conn+        result <- try (resetStmt conn 1) :: IO (Either SomeException ())+        case result of+            Left _ -> pure ()+            Right () -> do+                pending <- S.toList input+                assert (null pending) "reset marked connection reusable with prior result/reset packets still unread"+    ] +++    [ packetCase ("null-bitmap-" ++ show n) $ do+        let values=take n (cycle [MySQLNull,MySQLInt32 7])+            bitmap=makeNullMap values+        forM_ (zip [0..] values) $ \(i,v) -> equal "parameter NULL bit" (v==MySQLNull) (isColumnSet bitmap i)+    | n <- [0,1,2,7,8,9,15,16,17,63,64,65]+    ]
+ validation/hs/Validation/Reliability.hs view
@@ -0,0 +1,177 @@+{-# LANGUAGE ScopedTypeVariables #-}+module Validation.Reliability (cases) where++import Control.Concurrent+import Control.Exception hiding (assert)+import Control.Monad+import qualified Data.Text as T+import Database.MySQL.Base+import qualified Database.MySQL.TLS as TLS+import qualified System.IO.Streams as S+import System.Timeout+import Validation.Common+import qualified Validation.Stage3 as Stage3+import qualified Validation.Stage4 as Stage4+import qualified Validation.Stage6 as Stage6++cases :: [Case]+cases =+    [ Case "transaction.commit" "transactions" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_tx(n INT) ENGINE=InnoDB"+        withTransaction c (exec c "INSERT INTO mb_tx VALUES(7)")+        equal "commit persisted" 7 =<< scalarInt c "SELECT n FROM mb_tx"+    , Case "transaction.rollback" "transactions" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_tx(n INT) ENGINE=InnoDB"+        result <- try (withTransaction c (exec c "INSERT INTO mb_tx VALUES(7)" >> throwIO (userError "rollback-marker"))) :: IO (Either SomeException ())+        case result of Left _ -> pure (); Right () -> throwIO (userError "transaction did not raise")+        equal "rollback removed insert" 0 =<< scalarInt c "SELECT COUNT(*) FROM mb_tx"+        healthy c+    , Case "transaction.savepoint" "transactions" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_tx(n INT) ENGINE=InnoDB"+        withTransaction c $ do+            exec c "INSERT INTO mb_tx VALUES(7)"+            exec c "SAVEPOINT before_second"+            exec c "INSERT INTO mb_tx VALUES(8)"+            exec c "ROLLBACK TO SAVEPOINT before_second"+        equal "savepoint rollback" [[7]] . map (map number) =<< rows c "SELECT n FROM mb_tx"+    , Case "transaction.partial-result-exception" "transactions" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_tx(n INT) ENGINE=InnoDB"+        result <- try (withTransaction c $ do+            exec c "INSERT INTO mb_tx VALUES(7)"+            (_,stream) <- query_ c "SELECT seq FROM seq_1_to_1000"+            void (S.read stream)+            throwIO (userError "original-application-exception")) :: IO (Either SomeException ())+        case result of+            Left e -> assert ("original-application-exception" `T.isInfixOf` T.pack (displayException e))+                ("original error replaced by " ++ displayException e)+            Right () -> throwIO (userError "expected application exception")+        equal "rollback completed" 0 =<< scalarInt c "SELECT COUNT(*) FROM mb_tx"+    , Case "transaction.nested-preserves-outer" "transactions" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_tx(n INT) ENGINE=InnoDB"+        void (try (withTransaction c $ do+            exec c "INSERT INTO mb_tx VALUES(7)"+            withTransaction c (exec c "INSERT INTO mb_tx VALUES(8)")+            throwIO (userError "outer-rollback")) :: IO (Either SomeException ()))+        equal "nested call must not implicitly commit outer work" 0 =<< scalarInt c "SELECT COUNT(*) FROM mb_tx"+    , Case "transaction.read-committed-no-dirty-read" "transactions" $ withConn $ \a -> withConn $ \b -> do+        exec a "DROP TABLE IF EXISTS mb_isolation"+        exec a "CREATE TABLE mb_isolation(n INT) ENGINE=InnoDB"+        exec b "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED"+        exec a "BEGIN"+        exec a "INSERT INTO mb_isolation VALUES(7)"+        equal "uncommitted row invisible" 0 =<< scalarInt b "SELECT COUNT(*) FROM mb_isolation"+        exec a "COMMIT"+        equal "committed row visible" 1 =<< scalarInt b "SELECT COUNT(*) FROM mb_isolation"+        exec a "DROP TABLE mb_isolation"+    , Case "transaction.lock-timeout-and-reuse" "transactions" $ withConn $ \a -> withConn $ \b -> do+        exec a "DROP TABLE IF EXISTS mb_lock"+        exec a "CREATE TABLE mb_lock(id INT PRIMARY KEY,n INT) ENGINE=InnoDB"+        exec a "INSERT INTO mb_lock VALUES(1,7)"+        exec a "BEGIN"; exec a "UPDATE mb_lock SET n=8 WHERE id=1"+        exec b "SET innodb_lock_wait_timeout=1"+        expectERR [1205] (exec b "UPDATE mb_lock SET n=9 WHERE id=1")+        exec a "ROLLBACK"+        healthy b+        exec a "DROP TABLE mb_lock"+    , Case "transaction.deadlock-and-reuse" "transactions" $ withConn $ \a -> withConn $ \b -> do+        exec a "DROP TABLE IF EXISTS mb_deadlock"+        exec a "CREATE TABLE mb_deadlock(id INT PRIMARY KEY,n INT) ENGINE=InnoDB"+        exec a "INSERT INTO mb_deadlock VALUES(1,7),(2,8)"+        exec a "BEGIN"; exec b "BEGIN"+        exec a "UPDATE mb_deadlock SET n=n+1 WHERE id=1"+        exec b "UPDATE mb_deadlock SET n=n+1 WHERE id=2"+        done <- newEmptyMVar+        _ <- forkIO (try (exec a "UPDATE mb_deadlock SET n=n+1 WHERE id=2") >>= putMVar done)+        threadDelay 50000+        other <- try (exec b "UPDATE mb_deadlock SET n=n+1 WHERE id=1") :: IO (Either SomeException ())+        first <- takeMVar done :: IO (Either SomeException ())+        let isDeadlock (Left e)=case fromException e of Just (ERRException err)->errCode err==1213; Nothing->False+            isDeadlock _=False+        assert (isDeadlock first || isDeadlock other) "no deadlock error was returned"+        exec a "ROLLBACK"; exec b "ROLLBACK"+        healthy a; healthy b+        exec a "DROP TABLE mb_deadlock"+    , Case "transaction.ddl-implicit-commit" "transactions" $ withConn $ \c -> do+        exec c "DROP TABLE IF EXISTS mb_ddl_commit,mb_ddl_created"+        exec c "CREATE TABLE mb_ddl_commit(n INT) ENGINE=InnoDB"+        exec c "BEGIN"; exec c "INSERT INTO mb_ddl_commit VALUES(7)"+        exec c "CREATE TABLE mb_ddl_created(n INT)"+        exec c "ROLLBACK"+        equal "MariaDB DDL commits preceding work" 1 =<< scalarInt c "SELECT COUNT(*) FROM mb_ddl_commit"+        exec c "DROP TABLE mb_ddl_commit,mb_ddl_created"+    , Case "reliability.reset-unread-result" "reliability" $ withConn $ \c -> do+        st <- prepareStmt c "SELECT seq FROM seq_1_to_1000"+        (_,stream) <- queryStmt c st []+        void (S.read stream)+        result <- try (resetStmt c st) :: IO (Either SomeException ())+        case result of+            Left e | Just UnconsumedResultSet <- fromException e -> S.skipToEof stream+            Left e -> throwIO e+            Right () -> pure ()+        healthy c+    , Case "reliability.timeout-before-metadata" "reliability" $ withConn $ \c -> do+        result <- timeout 20000 (rows c "SELECT SLEEP(0.2)")+        equal "query timed out" Nothing result+        -- Contract correction approved 9 November 2026: cancellation while the+        -- reply is incomplete retires the wire. Reusing it could consume this+        -- command's late packets as another command's result. The original+        -- baseline retains its historical reuse expectation and failure.+        equal "cancelled transport retired" Broken =<< connectionStatus c+        rejected <- try (ping c)+        case rejected of+            Left (ConnectionUnavailable Broken) -> pure ()+            Left err -> throwIO err+            Right _ -> fail "cancelled connection admitted another command"+        withConn healthy+    , Case "reliability.server-kill-query" "reliability" $ withConn $ \c -> withConn $ \control -> do+        connectionId <- scalarInt c "SELECT CONNECTION_ID()"+        (_,stream) <- query_ c "SELECT REPEAT('x',20000),SLEEP(0.01) FROM seq_1_to_10000"+        void (S.read stream)+        exec control ("KILL QUERY " ++ show connectionId)+        expectERR [1317] (S.skipToEof stream)+        healthy c+    , Case "reliability.peer-disconnect" "reliability" $ withConn $ \c -> withConn $ \control -> do+        connectionId <- scalarInt c "SELECT CONNECTION_ID()"+        exec control ("KILL CONNECTION " ++ show connectionId)+        expectRejection (healthy c)+    , Case "reliability.unread-result-guard" "reliability" $ withConn $ \c -> do+        (_,stream) <- query_ c "SELECT seq FROM seq_1_to_1000"+        result <- try (ping c)+        case result of+            Left UnconsumedResultSet -> pure ()+            Right _ -> throwIO (userError "command allowed while rows remain unread")+        S.skipToEof stream+        healthy c+    , Case "reliability.tls-cancellation-propagates" "reliability" $ withTLS cancellation+    , Case "reliability.tcp-cancellation-propagates" "reliability" $ withConn cancellation+    , Case "connection.tls-untrusted-ca" "connection" $ do+        ci <- config+        params <- TLS.makeClientParams TLS.SystemCAStore+        expectRejection (withConnect (TLS.connect ci (params,"localhost")) healthy)+    , Case "connection.localhost-address-fallback" "connection" $ do+        ci <- config+        withConnect (connect ci {ciHost="localhost"}) healthy+    , Case "types.non-utf8-results" "types" $ withConn $ \c -> do+        exec c "SET character_set_results=latin1"+        equal "latin1 result" "é" . textOf =<< scalar c "SELECT CONVERT(0xE9 USING latin1)"+    , Case "capability.binary-bulk" "capabilities" $ Stage3.bulkFeature+    , Case "capability.metadata-cache" "capabilities" $ Stage3.cacheFeature+    , Case "capability.extended-metadata" "capabilities" $ Stage3.extendedFeature+    , Case "capability.session-tracking" "capabilities" $ Stage4.trackingFeature+    , Case "capability.reset-connection" "capabilities" $ Stage4.resetFeature+    , Case "capability.progress-reporting" "capabilities" Stage6.progressFeature+    , Case "capability.cursor-fetch" "capabilities" Stage6.cursorFeature+    , Case "capability.long-data" "capabilities" Stage6.longDataFeature+    , Case "capability.compression" "capabilities" Stage6.compressionFeature+    ]+  where+    cancellation c = do+        done <- newEmptyMVar+        thread <- forkIO (try (rows c "SELECT SLEEP(5)") >>= putMVar done)+        threadDelay 100000+        throwTo thread ThreadKilled+        result <- takeMVar done :: IO (Either SomeException [[MySQLValue]])+        case result of+            Left e | Just ThreadKilled <- fromException e -> pure ()+            Left e -> throwIO (userError ("cancellation replaced by " ++ displayException e))+            Right _ -> throwIO (userError "cancellation was swallowed")
+ validation/hs/Validation/Security.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE ScopedTypeVariables #-}+module Validation.Security (cases) where++import Control.Exception hiding (assert)+import Control.Monad+import Database.MySQL.Base+import qualified Database.MySQL.TLS as TLS+import qualified Network.TLS as WireTLS+import System.Environment+import Validation.Common+import qualified Validation.Stage7 as Stage7++clientCertificate :: Bool -> Bool -> ConnectInfo -> IO MySQLConn+clientCertificate expired explicitHook ci=do+    ca <- getEnv "MB_TLS_CA"+    cert <- getEnv (if expired then "MB_TLS_EXPIRED_CERT" else "MB_TLS_CLIENT_CERT")+    key <- getEnv (if expired then "MB_TLS_EXPIRED_KEY" else "MB_TLS_CLIENT_KEY")+    params <- TLS.makeClientParams' cert [] key (TLS.CustomCAStore ca)+    configured <- if explicitHook then do+        credential <- WireTLS.credentialLoadX509 cert key >>= either (throwIO . userError) pure+        pure params {WireTLS.clientHooks=(WireTLS.clientHooks params)+            {WireTLS.onCertificateRequest = \_ -> pure (Just credential)}}+        else pure params+    TLS.connect ci (configured,"localhost")++cases :: [Case]+cases =+    [ Case "auth.mutual-tls-helper-client" "authentication" $ do+        ci <- config+        withConnect (clientCertificate False False ci {ciUser="mariadb_x509"}) healthy+    , Case "auth.mutual-tls-explicit-certificate-hook" "authentication" $ do+        ci <- config+        withConnect (clientCertificate False True ci {ciUser="mariadb_x509"}) healthy+    , Case "auth.mutual-tls-client-required" "authentication" $ do+        ci <- config+        expectRejection (withConnect (tlsConnect ci {ciUser="mariadb_x509"} "localhost") healthy)+    , Case "auth.mutual-tls-expired-client" "authentication" $ do+        ci <- config+        expectRejection (withConnect (clientCertificate True True ci {ciUser="mariadb_x509"}) healthy)+    , Case "auth.unix-socket-plugin" "authentication" $ do+        ci <- config; path <- socketPath+        withConnect (connectUnixSocket path ci {ciUser="mariadb_socket",ciPassword=""}) healthy+    , Case "auth.unix-socket-rejects-tcp" "authentication" $ do+        ci <- config+        expectERR [1045,1698] (withConnect (connect ci {ciUser="mariadb_socket",ciPassword=""}) healthy)+    , Case "connection.mozilla-ca-store-installed" "connection" $ void (TLS.makeClientParams TLS.MozillaCAStore)+    , Case "auth.pam-environment" "environment" $ Stage7.pamFeature+    , Case "auth.gssapi-environment" "environment" $ Stage7.gssapiFeature+    ]
+ validation/hs/Validation/Stage1.hs view
@@ -0,0 +1,650 @@+{-# LANGUAGE ScopedTypeVariables #-}+module Validation.Stage1 (cases) where+import Control.Concurrent+import Control.Exception hiding (assert)+import Control.Monad+import Data.Binary.Put+import Data.IORef+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as L+import qualified Data.Text as T+import Data.Time.LocalTime (TimeOfDay(..))+import Database.MySQL.Base+import qualified Database.MySQL.Connection as C+import qualified Database.MySQL.BinLog as BinLog+import qualified System.IO.Streams as S+import System.Timeout+import Validation.Common+import Validation.FaultTransport++case1 :: String -> IO () -> Case+case1 ident = Case ("stage1."++ident) "stage1"++cases :: [Case]+cases =+    [ case1 "session.executable-comment-tracker-raw" $ withConn $ \c -> do+        exec c "/*M! SET session_track_system_variables='' */"+        expectContextUnknown c+        refreshSessionState c+        healthy c+    , case1 "session.executable-comment-tracker-prepared" $ withConn $ \c -> do+        bracket (prepareStmt c "/*M! SET session_track_system_variables='' */") (closeStmt c) $ \statement -> do+            -- PREPARE itself has no session effect; execution does.+            void (getRenderContext c)+            void (executeStmt c statement [])+            expectContextUnknown c+        refreshSessionState c+        healthy c+    , case1 "session.executable-comment-tracker-bound" $ withConn $ \c -> do+        void (execute c "/*M! SET session_track_system_variables=? */" [MySQLText ""])+        expectContextUnknown c+        refreshSessionState c+        healthy c+    , case1 "parameters.comment-digit-prefixes" $ withConn $ \c -> do+        forM_+            [ ("SELECT ? /*12345678901234567890*/", [MySQLInt32 7], 7)+            , ("SELECT /*!1 + ? */", [MySQLInt32 6], 7)+            , ("SELECT /*!1000007 + ? */", [MySQLInt32 2], 9)+            , ("SELECT ? /*!9999997 + ? */", [MySQLInt32 7], 7)+            , ("SELECT ? /*!999999 ignored /* nested ? */ + ? */", [MySQLInt32 7], 7)+            ] $ \(template, parameters, expected) -> do+                refreshSessionState c+                (_, stream) <- query c template parameters+                equal "server-compatible comment prefix" [[expected]] . map (map number) =<< S.toList stream+    , case1 "parameters.inactive-comment-context-retained" $ withConn $ \c -> do+        (_, first) <- query c "SELECT ? /*!999999 + ? */" [MySQLInt32 7]+        equal "inactive body is skipped" [[7]] . map (map number) =<< S.toList first+        (_, second) <- query c "SELECT ?" [MySQLInt32 8]+        equal "inactive comments need no refresh" [[8]] . map (map number) =<< S.toList second+    , case1 "parameters.server-prepared-lexer-parity" $ withConn $ \c -> do+        -- MariaDB's prepared-parameter count is independent of our text lexer.+        -- Compare actual execution too, normalizing only integer wire widths.+        forM_ ["", "NO_BACKSLASH_ESCAPES", "ANSI_QUOTES", "NO_BACKSLASH_ESCAPES,ANSI_QUOTES"] $ \mode -> do+            exec c ("SET sql_mode='" ++ mode ++ "'")+            forM_+                [ "SELECT ?", "SELECT '?',?", "SELECT 'it''s ?',?"+                , "SELECT 1 /* ? */,?", "SELECT 1 -- ?\n,?", "SELECT 1 # ?\n,?"+                , "SELECT `?`,? FROM (SELECT 1 AS `?`) t"+                , "SELECT /*!1 + ? */", "SELECT /*!1000007 + ? */"+                , "SELECT ? /*!50701 + ? */", "SELECT ? /*!9999997 + ? */"+                , "SELECT ? /*!999999 ignored /* nested ? */ + ? */"+                , "SELECT ? /*12345678901234567890*/"+                ] $ \template -> do+                    refreshSessionState c+                    bracket (prepareStmtDetail c template) (\(info,_,_) -> closeStmt c (stmtId info)) $ \(info,_,_) -> do+                        let parameters = replicate (stmtParamCnt info) (MySQLInt32 7)+                        (_, binary) <- queryStmt c (stmtId info) parameters+                        expected <- S.toList binary+                        -- An active executable comment is deliberately opaque+                        -- even when this fixture's expression is side-effect free.+                        refreshSessionState c+                        (_, text) <- query c template parameters+                        actual <- S.toList text+                        equal "prepared/text lexer parity" (map (map comparable) expected) (map (map comparable) actual)+    , case1 "parameters.batch-trailing-statement-delimiter" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_stage1_delimiter(n INT)"+        -- Connector/C independently confirms the final comment-only OK. Keep+        -- that reply; fixing the separator must not discard server results.+        forM_ [("INSERT INTO mb_stage1_delimiter VALUES(?);", [1,1]),+               ("INSERT INTO mb_stage1_delimiter VALUES(?); -- final comment", [1,1,0]),+               ("INSERT INTO mb_stage1_delimiter VALUES(?); # final comment", [1,1,0]),+               ("INSERT INTO mb_stage1_delimiter VALUES(?) /* semicolon ; */", [1,1])] $ \(template, expected) -> do+            exec c "TRUNCATE mb_stage1_delimiter"+            oks <- executeMany c template [[MySQLInt32 7], [MySQLInt32 8]]+            equal "all native server replies preserved" expected (map okAffectedRows oks)+            equal "all parameter sets inserted" 15 =<< scalarInt c "SELECT SUM(n) FROM mb_stage1_delimiter"+    , case1 "parameters.reversed-comments-rejected" $ do+        (c, sent, _) <- scripted defaultConnectionOptions []+        writeIORef (C.connectionRenderContext c) (Just defaultRenderContext {renderServerVersion=130200})+        forM_ ["SELECT ? /*!!999999 + ? */", "SELECT ? /*M!!999999 + ? */"] $ \template -> do+            result <- try (query c template [MySQLInt32 7])+            case result of+                Left (UnsupportedRenderingContext _) -> pure ()+                _ -> fail "unsupported reversed comment was rendered"+        equal "rejected before writing" [] =<< sent+        equal "pre-send rejection preserves connection" Ready =<< connectionStatus c+    , case1 "session.snapshot-value-bound" $ do+        let values = B.replicate 4097 65 : drop 1 validSnapshot+        (c, _, _) <- scripted defaultConnectionOptions (snapshotPackets values)+        expectRejection (refreshSessionState c)+        equal "oversized rendering fact retires connection" Broken =<< connectionStatus c+        (budgeted, _, _) <- scripted defaultConnectionOptions {maximumMetadataBytes=300}+            (snapshotPackets (B.replicate 512 65 : drop 1 validSnapshot))+        expectRejection (refreshSessionState budgeted)+        equal "snapshot also respects metadata budget" Broken =<< connectionStatus budgeted+    , case1 "session.snapshot-invalid-facts" $ do+        forM_ [take 4 validSnapshot ++ ["not-a-version", "67108864"],+               take 4 validSnapshot ++ ["0.0.0", "67108864"],+               take 5 validSnapshot ++ ["not-a-limit"]] $ \values -> do+            (c, _, _) <- scripted defaultConnectionOptions (snapshotPackets values)+            expectRejection (refreshSessionState c)+            equal "invalid facts fail inside owned exchange" Broken =<< connectionStatus c+    , case1 "session.snapshot-more-results-rejected" $ do+        let packets = take 9 (snapshotPackets validSnapshot) ++ [encodeToPacket 10 (EOF 0 10)]+        (c, _, _) <- scripted defaultConnectionOptions packets+        expectRejection (refreshSessionState c)+        equal "unexpected snapshot continuation cannot publish Ready" Broken =<< connectionStatus c+    , case1 "values.binary-time-microseconds-bounds" $ do+        let field = ColumnDef "" "" "" "v" "" 63 16 mySQLTypeTime 0 6+            packet micro = putToPacket 1+                (putWord8 12 >> putWord8 0 >> putWord32le 0 >> putWord8 0+                 >> putWord8 0 >> putWord8 0 >> putWord32le micro)+        forM_ [1000000, 2147483648, 4294967295] $ \micro ->+            expectRejection (getFromPacketExact (getBinaryField field) (packet micro))+        equal "highest valid fractional TIME" (MySQLTime 0 (TimeOfDay 0 0 0.999999))+            =<< getFromPacketExact (getBinaryField field) (packet 999999)+    , case1 "session.malformed-snapshot-retires" $ do+        let field = ColumnDef "" "" "" "v" "" 224 128 mySQLTypeString 0 0+            metadata = putToPacket 1 (putLenEncInt 6)+                : [encodeToPacket n field | n <- [2..7]] ++ [encodeToPacket 8 (EOF 0 2)]+            snapshot = ["", "utf8mb4", "utf8mb4", "utf8mb4_unicode_ci", "12.3.3", "67108864"]+            row values trailer = putToPacket 9 (mapM_ putLenEncBytes values >> trailer)+            endings = [encodeToPacket 10 (EOF 0 2)]+            malformed =+                [ textTranscript -- Wrong snapshot column count.+                , metadata ++ row snapshot (putWord8 0) : endings+                , metadata ++ row (B.singleton 255 : drop 1 snapshot) (pure ()) : endings+                ]+        forM_ malformed $ \packets -> do+            (c,_,_) <- scripted defaultConnectionOptions packets+            expectRejection (refreshSessionState c)+            equal "malformed snapshot retires before releasing ownership" Broken =<< connectionStatus c+    , case1 "ownership.completed-command-does-not-finish-successor" $ do+        -- Hold the successor in its writer while the completed action unwinds.+        -- Both success and exception paths must leave that new generation alone.+        forM_ [False, True] $ \failEarlier -> do+            (entered, release, completed) <- (,,) <$> newEmptyMVar <*> newEmptyMVar <*> newEmptyMVar+            input <- S.fromList [okPacket 1]+            c <- C.newConnection defaultConnectionOptions input+                (\_ -> putMVar entered () >> takeMVar release) (pure ())+            earlier <- try $ C.withCommand c $ do+                C.finishResponse c+                _ <- forkIO $ (try (ping c) :: IO (Either SomeException OK)) >>= putMVar completed+                takeMVar entered+                when failEarlier (throwIO (userError "completed-command-marker"))+            phase <- connectionStatus c+            putMVar release ()+            successor <- takeMVar completed+            equal "successor retains command ownership" Executing phase+            either throwIO (const (pure ())) successor+            case earlier of+                Left (err :: SomeException) -> assert failEarlier (displayException err)+                Right () -> assert (not failEarlier) "earlier exception disappeared"+            equal "successor completes normally" Ready =<< connectionStatus c+    , case1 "ownership.closed-cannot-become-streaming" $ do+        (c,_,_) <- scripted defaultConnectionOptions []+        close c+        expectRejection (C.markStreaming c)+        equal "closed is terminal" Closed =<< connectionStatus c+    , case1 "parameters.batch-trailing-line-comment" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_stage1_comment(n INT)"+        forM_ ["INSERT INTO mb_stage1_comment VALUES(?) -- final comment",+               "INSERT INTO mb_stage1_comment VALUES(?) # final comment"] $ \template -> do+            exec c "TRUNCATE mb_stage1_comment"+            oks <- executeMany c template [[MySQLInt32 7], [MySQLInt32 8]]+            equal "both statements execute" [1,1] (map okAffectedRows oks)+            equal "neither insert is swallowed by a comment" 15 =<< scalarInt c "SELECT SUM(n) FROM mb_stage1_comment"+    , case1 "cleanup.transport-cancellation-propagates" $ do+        input <- S.fromList []+        c <- C.newConnection defaultConnectionOptions input (const (pure ())) (throwIO ThreadKilled)+        result <- try (C.abortConnection c False)+        case result of+            Left ThreadKilled -> pure ()+            _ -> fail "transport cleanup swallowed cancellation"+        equal "retired before cancellation propagates" Broken =<< connectionStatus c+    , case1 "replication.setup-error-retires" $ do+        -- CDC setup now uses one atomic SET without discovery when ACK is off.+        -- Inject the same server error into that first setup command.+        let packets = [encodeToPacket 1 (ERR 1236 "HY000" "injected binlog failure")]+        (c,_,closed) <- scripted defaultConnectionOptions packets+        expectERR [1236] (BinLog.dumpBinLog c 123 (BinLog.BinLogTracker "binlog.000001" 4) False)+        equal "failed replication setup retires its owner" Broken =<< connectionStatus c+        equal "failed replication setup releases transport" 1 =<< closed+    , case1 "transaction.concurrent-close-commit-unknown" $ do+        reached <- newEmptyMVar; released <- newEmptyMVar; completed <- newEmptyMVar+        readCount <- newIORef (0::Int); writes <- newIORef (0::Int)+        input <- S.makeInputStream $ do+            n <- atomicModifyIORef' readCount (\x -> (x+1,x))+            if n==0 then pure (Just (okPacket 1))+            else putMVar reached () >> readMVar released >> throwIO NetworkException+        c <- C.newConnection defaultConnectionOptions input (\_ -> modifyIORef' writes (+1)) (putMVar released ())+        writeIORef (C.connectionServerStatus c) (Just 2)+        _ <- forkIO $ (try (withTransaction c (pure (7::Int))) :: IO (Either SomeException Int)) >>= putMVar completed+        takeMVar reached+        close c+        result <- takeMVar completed+        case result of Left err | Just (CommitOutcomeUnknown _) <- fromException err -> pure (); _ -> fail "concurrent close lost uncertain COMMIT outcome"+        equal "connection remains closed" Closed =<< connectionStatus c+        equal "one BEGIN and one COMMIT, no replay" 2 =<< readIORef writes+        failure <- connectionFailure c+        assert (maybe False commitOutcomeUncertain failure) "uncertain outcome missing from closed diagnostics"+    , case1 "transaction.closed-after-known-commit-error" $ do+        self <- newIORef Nothing+        readCount <- newIORef (0::Int)+        input <- S.makeInputStream $ do+            n <- atomicModifyIORef' readCount (\x -> (x+1,x))+            if n==0 then pure (Just (okPacket 1)) else do+                owner <- readIORef self+                maybe (fail "fixture connection not set") close owner+                pure (Just (encodeToPacket 1 (ERR 1180 "HY000" "known COMMIT failure")))+        c <- C.newConnection defaultConnectionOptions input (const (pure ())) (pure ())+        writeIORef self (Just c)+        writeIORef (C.connectionServerStatus c) (Just 2)+        result <- try (withTransaction c (pure (7::Int))) :: IO (Either SomeException Int)+        case result of Left err | Just (ERRException server) <- fromException err -> equal "known error preserved" 1180 (errCode server); _ -> fail "known terminal error became uncertain"+        equal "closed with complete reply" Closed =<< connectionStatus c+        failure <- connectionFailure c+        assert (maybe True (not . commitOutcomeUncertain) failure) "known error marked uncertain"+    , case1 "proxy.lost-commit-acknowledgement" $ withConn $ \control -> do+        exec control "DROP TABLE IF EXISTS mb_stage1_durable"+        exec control "CREATE TABLE mb_stage1_durable(n INT) ENGINE=InnoDB"+        ci <- config+        withFaultProxy ci DropCommitReply $ \proxyCi stats -> withConnect (connect proxyCi) $ \c -> do+            outcome <- try (withTransaction c (exec c "INSERT INTO mb_stage1_durable VALUES(7)"))+            case outcome of Left (CommitOutcomeUnknown _) -> pure (); _ -> fail "lost commit reply did not report unknown outcome"+            equal "durable row independently verified" 7 =<< scalarInt control "SELECT n FROM mb_stage1_durable"+            equal "no COMMIT replay" 1 . proxyCommits =<< stats+            equal "original connection retired" Broken =<< connectionStatus c+        exec control "DROP TABLE mb_stage1_durable"+    , case1 "proxy.fragmented-handshake-and-results" $ do+        ci <- config+        withFaultProxy ci (FragmentTraffic 1) $ \proxyCi _ -> withConnect (connect proxyCi) $ \c -> do+            equal "fragmented result" 937 =<< scalarInt c "SELECT 937"+            healthy c+    , case1 "proxy.disconnect-before-metadata" $ do+        ci <- config+        withFaultProxy ci DropTargetReply $ \proxyCi _ -> withConnect (connect proxyCi) $ \c -> do+            expectRejection (query_ c "SELECT 7 /*mb_stage1_fault*/")+            equal "retired before metadata" Broken =<< connectionStatus c+    , case1 "proxy.actual-command-counts" $ do+        ci <- config+        withFaultProxy ci (FragmentTraffic 65536) $ \proxyCi stats -> withConnect (connect proxyCi) $ \c -> do+            let count action expected = do+                    before <- proxyCommands <$> stats+                    _ <- action+                    after <- proxyCommands <$> stats+                    equal "wire command count" expected (after-before)+                param = query c "SELECT ?" [MySQLInt32 7] >>= S.skipToEof . snd+            count (param >> param) 2+            count (exec c "DO 1" >> param) 2+            count (withTransaction c (pure ())) 2+            count (void (executeMany c "DO ?" [[MySQLInt32 1],[MySQLInt32 2]])) 1+            exec c "SET @@session.session_track_system_variables=''"+            count (expectContextUnknown c) 0+            count (refreshSessionState c) 2+            count param 1+    , case1 "session.tracker-selection-preserved" $ withConn $ \c -> do+        exec c "SET session_track_system_variables='time_zone'"+        refreshSessionState c+        actual <- rows c "SELECT @@session.session_track_system_variables"+        let bytes=case actual of [[MySQLText value]] -> T.splitOn "," value; _ -> []+        assert (all (`elem` bytes) ["time_zone","sql_mode","character_set_client","character_set_connection"]) "session tracker selections lost"+        exec c "SET session_track_system_variables='*'"+        refreshSessionState c+        equal "wildcard preserved" [[MySQLText "*"]] =<< rows c "SELECT @@session.session_track_system_variables"+    , case1 "session.unknown-tracking-kind-skipped" $ do+        let reply=putToPacket 1 (putWord8 0 >> putWord8 0 >> putWord8 0 >> putWord16le 0x4002 >> putWord16le 0 >> putLenEncBytes "" >> putLenEncBytes (L.toStrict (runPut (putWord8 42 >> putLenEncBytes "opaque"))))+        (c,_,_) <- scripted defaultConnectionOptions [reply]+        writeIORef (C.connectionCapabilities c) 0x1800000+        writeIORef (C.connectionRenderContext c) (Just defaultRenderContext)+        void (ping c)+        equal "known context retained" defaultRenderContext =<< getRenderContext c+    , case1 "cleanup.absolute-deadline-preserves-primary" $ do+        packets <- newIORef (okPacket 1 : take 4 textTranscript)+        stalled <- newEmptyMVar+        input <- S.makeInputStream $ do+            next <- atomicModifyIORef' packets (\xs -> case xs of [] -> ([],Nothing); x:rest -> (rest,Just x))+            case next of Just x -> pure (Just x); Nothing -> takeMVar stalled+        c <- C.newConnection defaultConnectionOptions {cleanupTimeoutMicros=20000} input (const (pure ())) (pure ())+        writeIORef (C.connectionServerStatus c) (Just 2)+        result <- timeout 1000000 $ try $ withTransaction c $ do+            (_,stream) <- query_ c "SELECT 7"+            void (S.read stream)+            throwIO (userError "primary-cleanup-marker")+        case result of Just (Left (err :: SomeException)) -> assert ("primary-cleanup-marker" `T.isInfixOf` T.pack (displayException err)) "primary replaced"; _ -> fail "cleanup deadline failed"+        equal "stalled cleanup retired" Broken =<< connectionStatus c+    , case1 "fault.wrong-first-sequence" $ do+        (c,_,closed) <- scripted defaultConnectionOptions [okPacket 2]+        expectRejection (ping c)+        equal "connection retired" Broken =<< connectionStatus c+        equal "one abort" 1 =<< closed+        close c; close c+        equal "idempotent release" 1 =<< closed+    , case1 "fragments.physical-exchange-sequence-wrap" $ do+        (c,_,_) <- scripted defaultConnectionOptions [Packet 1 n "x" | n <- [254,255,0]]+        C.setExchangeSequence c 254+        actual <- replicateM 3 (pSeqN <$> C.readConnPacket c)+        equal "physical sequence wraps modulo 256" [254,255,0] actual+    , case1 "fault.eof-metadata-validates" $ do+        let packets=take 2 textTranscript ++ [okPacket 3]+        (c,_,_) <- scripted defaultConnectionOptions packets+        expectRejection (query_ c "SELECT 7")+        equal "malformed metadata retires" Broken =<< connectionStatus c+    , case1 "fault.old-stream-cannot-read-new-command" $ do+        (c,sent,_) <- scripted defaultConnectionOptions textTranscript+        (_,stream) <- query_ c "SELECT 7"+        C.finishResponse c -- fixture simulates invalidation before another generation+        closeStmt c 123+        expectRejection (S.read stream)+        equal "exactly two commands" 2 . length =<< sent+    , case1 "fault.unread-close-sends-nothing" $ do+        (c,sent,_) <- scripted defaultConnectionOptions textTranscript+        (_,stream) <- query_ c "SELECT 7"+        expectRejection (closeStmt c 123)+        equal "no close bytes" 1 . length =<< sent+        equal "original stream intact" [[MySQLInt32 7]] =<< S.toList stream+        equal "repeated EOF" Nothing =<< S.read stream+    , case1 "fault.server-error-reusable" $ do+        (c,_,_) <- scripted defaultConnectionOptions [encodeToPacket 1 (ERR 1064 "42000" "syntax"),okPacket 1]+        expectERR [1064] (ping c)+        void (ping c)+        equal "terminal ERR allows reuse" Ready =<< connectionStatus c+    , case1 "fault.partial-write-retires" $ do+        input <- S.fromList []+        closed <- newIORef (0::Int)+        c <- C.newConnection defaultConnectionOptions input (\_ -> throwIO (userError "injected partial write")) (modifyIORef' closed (+1))+        expectRejection (ping c)+        equal "broken" Broken =<< connectionStatus c+        equal "released" 1 =<< readIORef closed+    , case1 "fault.pre-send-limit-is-clean" $ do+        (c,sent,_) <- scripted defaultConnectionOptions {maximumMessageBytes=8} []+        expectRejection (execute_ c "DO 12345678901234567890")+        equal "no bytes sent" [] =<< sent+        equal "clean connection" Ready =<< connectionStatus c+    , case1 "fault.metadata-budget" $ do+        (c,_,_) <- scripted defaultConnectionOptions {maximumMetadataBytes=1} textTranscript+        expectRejection (query_ c "SELECT 7")+        equal "over-limit response retired" Broken =<< connectionStatus c+    , case1 "fault.column-limit" $ do+        (c,_,_) <- scripted defaultConnectionOptions {maximumColumns=1} [putToPacket 1 (putLenEncInt 2)]+        expectRejection (query_ c "SELECT 7,8")+        equal "count rejected" Broken =<< connectionStatus c+    , case1 "fault.ok-terminator-binary-zero-row" $ do+        let col=ColumnDef "" "" "" "n" "" 63 11 mySQLTypeLong 0 0+            end=putToPacket 4 (putWord8 254 >> putWord8 0 >> putWord8 0 >> putWord16le 2 >> putWord16le 0)+            row=putToPacket 3 (putWord8 0 >> putWord8 0 >> putWord32le 7)+        (c,_,_) <- scripted defaultConnectionOptions [putToPacket 1 (putLenEncInt 1),encodeToPacket 2 col,row,end]+        writeIORef (C.connectionCapabilities c) 0x1000000+        (_,stream) <- queryStmt c 1 []+        equal "binary row is not command OK" [[MySQLInt32 7]] =<< S.toList stream+        equal "finished" Ready =<< connectionStatus c+    , case1 "fault.concurrent-command" $ do+        input <- S.fromList [okPacket 1]+        entered <- newEmptyMVar; release <- newEmptyMVar; done <- newEmptyMVar+        c <- C.newConnection defaultConnectionOptions input (\_ -> putMVar entered () >> takeMVar release) (pure ())+        _ <- forkIO ((try (ping c) :: IO (Either SomeException OK)) >>= putMVar done)+        takeMVar entered+        expectRejection (ping c)+        putMVar release ()+        result <- takeMVar done+        either throwIO (const (pure ())) result+        equal "owner completed" Ready =<< connectionStatus c+    , case1 "transaction.thread-exclusion" $ withConn $ \c -> do+        withTransaction c $ do+            result <- newEmptyMVar+            _ <- forkIO ((try (ping c) :: IO (Either ConnectionError OK)) >>= putMVar result)+            other <- takeMVar result+            case other of Left ConcurrentConnectionUse -> pure (); _ -> fail "transaction admitted another thread"+            equal "owner still works" 7 =<< scalarInt c "SELECT 7"+        healthy c+    , case1 "transaction.caught-nested-rejection" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_stage1_tx(n INT) ENGINE=InnoDB"+        withTransaction c $ do+            result <- try (withTransaction c (exec c "INSERT INTO mb_stage1_tx VALUES(8)"))+            case result of Left NestedTransactionUnsupported -> pure (); _ -> fail "nested transaction accepted"+            exec c "INSERT INTO mb_stage1_tx VALUES(7)"+        equal "outer work commits" 7 =<< scalarInt c "SELECT SUM(n) FROM mb_stage1_tx"+    , case1 "transaction.manual-begin-rejected" $ withConn $ \c -> do+        exec c "BEGIN"+        result <- try (withTransaction c (pure ()))+        case result of Left NestedTransactionUnsupported -> pure (); _ -> fail "manual transaction not detected"+        exec c "ROLLBACK"+        healthy c+    , case1 "transaction.returned-unread-rolls-back" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_stage1_tx(n INT) ENGINE=InnoDB"+        expectRejection $ withTransaction c $ do+            exec c "INSERT INTO mb_stage1_tx VALUES(7)"+            query_ c "SELECT seq FROM seq_1_to_1000"+        equal "nothing committed" 0 =<< scalarInt c "SELECT COUNT(*) FROM mb_stage1_tx"+    , case1 "timeout.connection-retired" $ withConn $ \c -> do+        result <- timeout 20000 (rows c "SELECT SLEEP(0.2)")+        equal "timeout propagated" Nothing result+        equal "retired" Broken =<< connectionStatus c+        expectRejection (ping c)+        withConn healthy+    , case1 "session.row-count-no-hidden-probe" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_stage1_rowcount(n INT)"+        exec c "INSERT INTO mb_stage1_rowcount VALUES(1),(2),(3)"+        (_,stream) <- query c "SELECT ROW_COUNT(),?" [MySQLInt32 9]+        actual <- S.toList stream+        equal "preceding statement preserved" [[3,9]] (map (map number) actual)+    , case1 "session.unknown-context-explicit-refresh" $ withConn $ \c -> do+        expectERR [1146] (exec c "SELECT * FROM mb_stage1_missing_table")+        result <- try (query c "SELECT ?" [MySQLInt32 7])+        case result of Left SessionContextUnavailable -> pure (); _ -> fail "unknown context was silently refreshed"+        refreshSessionState c+        (_,stream) <- query c "SELECT ?" [MySQLInt32 7]+        equal "explicit refresh restores parameters" [[7]] . map (map number) =<< S.toList stream+    , case1 "session.tracker-replacement-is-unknown" $ withConn $ \c -> do+        exec c "SET `session_track_system_variables`=''"+        expectContextUnknown c+        refreshSessionState c+        (_,stream) <- query c "SELECT ?" [MySQLInt32 7]+        equal "refresh restores tracking" [[7]] . map (map number) =<< S.toList stream+    , case1 "session.raw-multiple-invalidates" $ withConn $ \c -> do+        void (executeMany_ c "DO 1; DO 2")+        expectContextUnknown c+        refreshSessionState c+        healthy c+    , case1 "session.raw-routine-fast-path-excluded" $ do+        (c,_,_) <- scripted defaultConnectionOptions []+        writeIORef (C.connectionRenderContext c) (Just defaultRenderContext)+        forM_ ["SELECT mb_stage1_context_fn()","SELECT db.mb_stage1_context_fn()","INSERT INTO t(n) VALUES(mb_stage1_context_fn())","SELECT `mb_stage1_context_fn`()"] $ \statement -> do+            C.classifyQueryEffect c statement+            equal "opaque call excluded" (Just True) =<< readIORef (C.connectionQueryEffect c)+        C.classifyQueryEffect c "INSERT INTO t(n) VALUES(1),(2),(3)"+        equal "flat numeric insert fast path" (Just False) =<< readIORef (C.connectionQueryEffect c)+    , case1 "session.prepared-tracker-replacement-is-unknown" $ withConn $ \c -> do+        bracket (prepareStmt c "SET session_track_system_variables=''") (closeStmt c) $ \st -> void (executeStmt c st [])+        expectContextUnknown c+        refreshSessionState c+        healthy c+    , case1 "session.mode-changes-both-directions" $ withConn $ \c -> do+        forM_ ["NO_BACKSLASH_ESCAPES","","NO_BACKSLASH_ESCAPES,ANSI_QUOTES",""] $ \mode -> do+            exec c ("SET sql_mode='"++mode++"'")+            (_,stream) <- query c "SELECT ?" [MySQLText "a'\\\0b"]+            equal "literal preserved" [[MySQLText "a'\\\0b"]] =<< S.toList stream+    , case1 "session.latin1-parameter" $ withConn $ \c -> do+        exec c "SET NAMES latin1"+        (_,stream) <- query c "SELECT ?" [MySQLText "é€"]+        equal "cp1252 bound text" [[MySQLText "é€"]] =<< S.toList stream+    , case1 "session.latin1-prepared-parameter" $ withConn $ \c -> do+        exec c "SET NAMES latin1"+        equal "prepared cp1252 text" [[MySQLText "é€"]] =<< preparedRows c "SELECT ?" [MySQLText "é€"]+    , case1 "cleanup.second-cancellation-propagates" $ do+        packets <- newIORef (okPacket 1 : take 4 textTranscript)+        reached <- newEmptyMVar; blocked <- newEmptyMVar; completed <- newEmptyMVar+        input <- S.makeInputStream $ do+            next <- atomicModifyIORef' packets (\xs -> case xs of [] -> ([],Nothing); x:rest -> (rest,Just x))+            case next of Just x -> pure (Just x); Nothing -> putMVar reached () >> takeMVar blocked+        c <- C.newConnection defaultConnectionOptions input (const (pure ())) (pure ())+        writeIORef (C.connectionServerStatus c) (Just 2)+        tid <- forkIO $ (try (withTransaction c $ do+            (_,stream) <- query_ c "SELECT 7"+            void (S.read stream)+            throwIO (userError "earlier application failure")) :: IO (Either SomeException ())) >>= putMVar completed+        takeMVar reached+        throwTo tid ThreadKilled+        result <- takeMVar completed+        case result of Left e | Just ThreadKilled <- fromException e -> pure (); _ -> fail "new cancellation was replaced"+        equal "cancelled cleanup retired" Broken =<< connectionStatus c+    , case1 "values.invalid-parameter-before-send" $ do+        (c,sent,_) <- scripted defaultConnectionOptions []+        writeIORef (C.connectionRenderContext c) (Just defaultRenderContext)+        expectRejection (query c "SELECT ?" [MySQLDouble (1/0)])+        equal "no command bytes" [] =<< sent+        equal "still ready" Ready =<< connectionStatus c+    , case1 "session.legacy-eof-integration" $ do+        ci <- config+        withConnect (connectWithOptions defaultConnectionOptions {enableSessionTracking=False} ci) $ \c -> do+            healthy c+            equal "legacy prepared results" [[7]] . map (map number) =<< preparedRows c "SELECT 7" []+            refreshSessionState c+            (_,stream) <- query c "SELECT ?" [MySQLInt32 9]+            equal "explicit legacy context" [[9]] . map (map number) =<< S.toList stream+    , case1 "fault.row-trailing-bytes" $ do+        let packets=take 3 textTranscript ++ [putToPacket 4 (putLenEncBytes "7" >> putWord8 0)]+        (c,_,_) <- scripted defaultConnectionOptions packets+        (_,stream) <- query_ c "SELECT 7"+        expectRejection (S.read stream)+        equal "malformed row retired" Broken =<< connectionStatus c+    , case1 "parameters.quoted-semicolon-batch" $ withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_stage1_quote(v TEXT,n INT)"+        oks <- executeMany c "INSERT INTO mb_stage1_quote VALUES(';?',?)" [[MySQLInt32 7],[MySQLInt32 8]]+        equal "two statements" [1,1] (map okAffectedRows oks)+        equal "correct bound data" 15 =<< scalarInt c "SELECT SUM(n) FROM mb_stage1_quote"+    , case1 "parameters.opaque-routine-batch-rejected" $ withConn $ \c ->+        expectRejection (executeMany c "SELECT mb_stage1_unknown_routine(?)" [[MySQLInt32 7]])+    , case1 "parameters.quoted-routine-batch-rejected" $ do+        (c,sent,_) <- scripted defaultConnectionOptions []+        writeIORef (C.connectionRenderContext c) (Just defaultRenderContext)+        result <- try (executeMany c "SELECT `mb_stage1_unknown_routine`(?)" [[MySQLInt32 7]])+        case result of Left AmbiguousParameterizedBatch -> pure (); _ -> fail "quoted routine passed batch classifier"+        equal "no bytes before rejection" [] =<< sent+    , case1 "session.unencodable-clean" $ withConn $ \c -> do+        exec c "SET NAMES latin1"+        expectRejection (query c "SELECT ?" [MySQLText "😀"])+        equal "pre-send failure clean" Ready =<< connectionStatus c+        healthy c+    , case1 "parameters.batch-ambiguous-rejected-before-send" $ withConn $ \c -> do+        exec c "SET @mb_stage1_marker=0"+        expectRejection (queryMulti c "SET @mb_stage1_marker=?; SELECT ?" [MySQLInt32 1,MySQLInt32 2])+        equal "no partial execution" 0 =<< scalarInt c "SELECT @mb_stage1_marker"+    , case1 "parameters.injection-remains-data" $ withConn $ \c -> do+        forM_ ["'; SELECT 9; -- ","\\' OR 1=1 #","?; /*M! SELECT 3 */"] $ \text -> do+            (_,stream) <- query c "SELECT ?" [MySQLText text]+            equal "data" [[MySQLText text]] =<< S.toList stream+    , case1 "parameters.executable-comment-active" $ withConn $ \c -> do+        (_,stream) <- query c "SELECT 1 /*M! + ? */" [MySQLInt32 6]+        equal "active comment parameter" [[7]] . map (map number) =<< S.toList stream+    , case1 "parameters.executable-comment-inactive" $ withConn $ \c -> do+        (_,stream) <- query c "SELECT ? /*!50701 + ? */" [MySQLInt32 7]+        equal "inactive question ignored" [[7]] . map (map number) =<< S.toList stream+    , case1 "values.invalid-utf8-rejected" $ do+        let col=ColumnDef "" "" "" "v" "" 224 12 mySQLTypeString 0 0+        expectRejection (getFromPacket (getTextField col) (putToPacket 1 (putLenEncBytes (B.pack [255]))))+    , case1 "values.invalid-utf8-rejected-before-yield" $ do+        let col=ColumnDef "" "" "" "v" "" 224 12 mySQLTypeString 0 0+        (c,_,_) <- scripted defaultConnectionOptions [putToPacket 1 (putLenEncInt 1),encodeToPacket 2 col,encodeToPacket 3 (EOF 0 2),putToPacket 4 (putLenEncBytes (B.pack [255]))]+        (_,stream) <- query_ c "SELECT 'value'"+        expectRejection (void (S.read stream))+        equal "bad value retires at IO boundary" Broken =<< connectionStatus c+    , case1 "fault.trailing-row-unfed-chunk" $ do+        let bad=Packet 3 4 (L.fromChunks [B.pack [1,55],B.singleton 0])+        (c,_,_) <- scripted defaultConnectionOptions (take 3 textTranscript ++ [bad])+        (_,stream) <- query_ c "SELECT 7"+        expectRejection (S.read stream)+        equal "unfed bytes rejected" Broken =<< connectionStatus c+    , case1 "fault.logical-limit-before-continuation-body" $ do+        calls <- newIORef (0::Int)+        blocked <- newEmptyMVar+        source <- S.makeInputStream $ do+            n <- atomicModifyIORef' calls (\x -> (x+1,x))+            case n of+                0 -> pure (Just (B.pack [255,255,255,1]))+                1 -> pure (Just (B.replicate 16777215 120))+                2 -> pure (Just (B.pack [2,0,0,2]))+                _ -> takeMVar blocked+        packets <- C.decodeInputStreamWithLimit 16777216 source+        outcome <- timeout 1000000 (try (C.readPacketWithLimit 16777216 packets) :: IO (Either ConnectionError Packet))+        case outcome of Just (Left (ProtocolError _)) -> pure (); _ -> fail "quota not enforced before body read"+        equal "only first body and two headers read" 3 =<< readIORef calls+    , case1 "fault.missing-tracking-payload" $ do+        (c,_,_) <- scripted defaultConnectionOptions [putToPacket 1 (putWord8 0 >> putWord8 0 >> putWord8 0 >> putWord16le 0x4002 >> putWord16le 0)]+        writeIORef (C.connectionCapabilities c) 0x1800000+        expectRejection (ping c)+        equal "malformed OK retires" Broken =<< connectionStatus c+    , case1 "fault.session-envelope-budget" $ do+        let reply=putToPacket 1 (putWord8 0 >> putWord8 0 >> putWord8 0 >> putWord16le 2 >> putWord16le 0 >> putLenEncBytes "0123456789")+        (c,_,_) <- scripted defaultConnectionOptions {maximumMetadataBytes=8} [reply]+        writeIORef (C.connectionCapabilities c) 0x1800000+        expectRejection (ping c)+        equal "oversize envelope retired" Broken =<< connectionStatus c+    , case1 "parameters.oracle-mode-quotes" $ withConn $ \c -> do+        exec c "SET sql_mode='ORACLE'"+        (_,stream) <- query c "SELECT 'don''t ?',?" [MySQLInt32 7]+        equal "Oracle mode quote keeps its question" [[MySQLText "don't ?",MySQLInt32 7]] =<< S.toList stream+    , case1 "parameters.ansi-identifier-backslash" $ withConn $ \c -> do+        exec c "SET sql_mode='ANSI_QUOTES'"+        (_,stream) <- query c "SELECT \"path\\\",? FROM (SELECT 1 AS \"path\\\") t" [MySQLInt32 7]+        equal "identifier backslash is literal" [[1,7]] . map (map number) =<< S.toList stream+    , case1 "values.unmapped-charset-exact-bytes" $ do+        let col=ColumnDef "" "" "" "v" "" 35 12 mySQLTypeString 0 0+        equal "ucs2 preserved" (MySQLBytes (B.pack [0,233])) =<< getFromPacket (getTextField col) (putToPacket 1 (putLenEncBytes (B.pack [0,233])))+    , case1 "values.raw-dates-text-prepared" $ withConn $ \c -> do+        exec c "SET sql_mode='ALLOW_INVALID_DATES'"+        exec c "CREATE TEMPORARY TABLE mb_stage1_date(d DATE,dt DATETIME(6))"+        let d=DateParts 2023 2 31; value=MySQLDateRaw d+        void (execute c "INSERT INTO mb_stage1_date(d) VALUES(?)" [value])+        equal "text date" [[value]] =<< rows c "SELECT d FROM mb_stage1_date"+        equal "binary date" [[value]] =<< preparedRows c "SELECT d FROM mb_stage1_date" []+        exec c "TRUNCATE mb_stage1_date"+        void (bracket (prepareStmt c "INSERT INTO mb_stage1_date(d) VALUES(?)") (closeStmt c) (\st -> executeStmt c st [MySQLDateRaw (DateParts 0 0 0)]))+        equal "zero preserved" [[MySQLDateRaw (DateParts 0 0 0)]] =<< rows c "SELECT d FROM mb_stage1_date"+    ] +++    [ case1 ("fragments.limit-"++show limit) $ do+        input <- S.fromList [Packet 5 1 "abcde"]+        if limit<5 then expectRejection (C.readPacketWithLimit limit input) else equal "accepted size" 5 . pLen =<< C.readPacketWithLimit limit input+    | limit <- [4,5,6]+    ] +++    [ case1 ("values.uint64-"++show n) $ do+        let packet=putToPacket 1 (putWord8 0 >> putLenEncWord64 n >> putLenEncWord64 n >> putWord16le 2 >> putWord16le 0)+        ok <- getFromPacket getOK packet+        equal "affected" n (okAffectedRows ok); equal "insert id" n (okLastInsertID ok)+    | n <- [9223372036854775807,9223372036854775808,18446744073709551615]+    ] +++    [ case1 ("parameters.quote-"++show i) $ withConn $ \c -> do+        (_,stream) <- query c (sql template) [MySQLInt32 7]+        actual <- S.toList stream+        case actual of+            [values] -> case reverse values of value:_ -> equal "bound final cell" 7 (number value); [] -> fail "empty row"+            _ -> fail "expected one row"+    | (i,template) <- zip [1::Int ..]+        ["SELECT '?',?","SELECT 'it''s ?',?","SELECT `?`,? FROM (SELECT 1 AS `?`) t","SELECT 1 /* ? */,?","SELECT 1 -- ?\n,?","SELECT 1 # ?\n,?","SELECT ';?',?"]+    ]++expectContextUnknown :: MySQLConn -> IO ()+expectContextUnknown c = do+    result <- try (query c "SELECT ?" [MySQLInt32 7])+    case result of+        Left SessionContextUnavailable -> pure ()+        Right (_, stream) -> do+            -- Preserve the assertion failure: leaving this accidental success+            -- unread would let a bracketed closeStmt mask it with another error.+            S.skipToEof stream+            fail "unknown context silently reused"+        _ -> fail "unexpected connection error"++-- Fixed six-column snapshot transcript, independent of the live server. Values+-- remain ordinary wire strings so malformed-fact tests exercise real decoding.+validSnapshot :: [B.ByteString]+validSnapshot = ["", "utf8mb4", "utf8mb4", "utf8mb4_unicode_ci", "12.3.3", "67108864"]++snapshotPackets :: [B.ByteString] -> [Packet]+snapshotPackets values =+    putToPacket 1 (putLenEncInt 6)+    : [encodeToPacket n field | n <- [2..7]]+    ++ [ encodeToPacket 8 (EOF 0 2)+       , putToPacket 9 (mapM_ putLenEncBytes values)+       , encodeToPacket 10 (EOF 0 2)+       ]+  where+    field = ColumnDef "" "" "" "v" "" 224 8192 mySQLTypeString 0 0++-- Prepared and rendered numeric expressions can legitimately use different+-- integer widths. Keep text, bytes and NULL distinct while comparing values.+comparable :: MySQLValue -> (String, String)+comparable (MySQLText text) = ("text", T.unpack text)+comparable (MySQLBytes bytes) = ("bytes", show bytes)+comparable MySQLNull = ("null", "")+comparable value = ("number", show (number value))
+ validation/hs/Validation/Stage2.hs view
@@ -0,0 +1,275 @@+{-# LANGUAGE ScopedTypeVariables #-}+-- | Additive native-connection regressions. Wire fixtures and the server's+-- published signing vector are independent of the client encoder under test.+module Validation.Stage2 (cases) where++import Control.Concurrent+import Control.Exception hiding (assert)+import Control.Monad+import Data.Binary (put)+import Data.Binary.Put+import Data.Bits+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as BC+import qualified Data.ByteString.Lazy as L+import Data.Word (Word8)+import GHC.Clock (getMonotonicTimeNSec)+import Data.IORef+import Database.MySQL.Base+import qualified Database.MySQL.Connection as C+import qualified Database.MySQL.TLS as TLS+import qualified Network.Socket as N+import qualified Network.Socket.ByteString as NB+import qualified Network.TLS as WireTLS+import System.Environment+import System.FilePath+import qualified System.IO.Streams as S+import System.Timeout+import Validation.Common++case2 :: String -> IO () -> Case+case2 name = Case ("stage2." ++ name) "stage2-native-connections"++greeting :: B.ByteString -> B.ByteString -> Greeting+greeting plugin nonce = Greeting 10 "12.3.3-MariaDB" 1 (B.take 8 nonce)+    (clientCap .|. 0x1800000) 224 2 (B.drop 8 nonce) plugin++ok :: Word8 -> Packet+ok seqN = putToPacket seqN (putByteString "\0\0\0\2\0\0\0")++switch :: Word8 -> B.ByteString -> B.ByteString -> Packet+switch seqN plugin nonce = putToPacket seqN+    (putWord8 254 >> putByteString plugin >> putWord8 0 >> putByteString nonce)++-- The first packet has already been read by the constructor. All following+-- packets pass through the real sequence-checked connection reader/writer.+conversation :: Greeting -> Packet -> [Packet] -> IO (IORef [Packet], IO ())+conversation initial first rest = do+    input <- S.fromList rest+    writes <- newIORef []+    conn <- C.newConnection defaultConnectionOptions input+        (\p -> modifyIORef' writes (p:)) (pure ())+    C.setExchangeSequence conn (fromIntegral (pSeqN first + 1))+    pure (writes, C.completeAuth conn initial True "foobar" first)++rejectAuth :: IO a -> IO ()+rejectAuth action = do+    result <- try action+    case result of+        Left (_ :: AuthException) -> pure ()+        Right _ -> throwIO (userError "invalid authentication unexpectedly succeeded")++withListener :: (N.Socket -> Int -> IO a) -> IO a+withListener action = bracket (N.socket N.AF_INET N.Stream 0) N.close $ \listener -> do+    N.bind listener (N.SockAddrInet 0 (N.tupleToHostAddress (127,0,0,1)))+    N.listen listener 1+    N.SockAddrInet port _ <- N.getSocketName listener+    action listener (fromIntegral port)++checkSNI :: Bool -> IO ()+checkSNI enabled = withListener $ \listener port -> do+    ca <- getEnv "MB_TLS_CA"+    observed <- newIORef Nothing+    params <- TLS.makeServerParams (takeDirectory ca </> "server-cert.pem") []+        (takeDirectory ca </> "server-key.pem")+    let serverParams = params {WireTLS.serverHooks=(WireTLS.serverHooks params)+            {WireTLS.onServerNameIndication= \name -> writeIORef observed name >> pure mempty}}+        recvExactly sock count = go count []+          where+            go 0 chunks = pure (B.concat (reverse chunks))+            go n chunks = do+                chunk <- NB.recv sock n+                when (B.null chunk) (throwIO (userError "truncated TLS request"))+                go (n-B.length chunk) (chunk:chunks)+    finished <- newEmptyMVar+    -- The actual worker writes a complete native greeting and reads exactly+    -- the unencrypted SSLRequest before handing the socket to tls.+    worker <- forkFinally (bracket (fst <$> N.accept listener) N.close $ \sock -> do+        let hello=(greeting "mysql_native_password" (B.replicate 20 65))+                {greetingCaps=clientCap .|. 0x1800800}+        NB.sendAll sock (L.toStrict (runPut (put (encodeToPacket 0 hello))))+        request <- recvExactly sock 36+        equal "SSLRequest sequence" 1 (B.index request 3)+        bracket (WireTLS.contextNew sock serverParams) WireTLS.contextClose $ \ctx ->+            WireTLS.handshake ctx >> WireTLS.bye ctx) (putMVar finished)+    flip finally (killThread worker) $ do+        ci <- config+        clientParams <- TLS.makeClientParams (TLS.CustomCAStore ca)+        outcome <- try (TLS.connectWithOptions defaultConnectionOptions {connectTimeoutMicros=1000000}+            ci {ciHost="127.0.0.1",ciPort=fromIntegral port} (clientParams {WireTLS.clientUseServerNameIndication=enabled},"localhost"))+        case outcome of+            Left (_ :: SomeException) -> pure ()+            Right conn -> close conn >> throwIO (userError "fixture unexpectedly authenticated")+        result <- timeout 2000000 (takeMVar finished)+        case result of+            Just (Right ()) -> pure ()+            _ -> throwIO (userError "fixture TLS handshake did not finish")+        equal "SNI host" (if enabled then Just "localhost" else Nothing) =<< readIORef observed++cases :: [Case]+cases =+    [ case2 "capabilities.greeting-upper-word" $ do+        let expected = (greeting "mysql_native_password" (B.replicate 20 65))+                {greetingCaps=clientCap .|. bit 35 .|. bit 63}+        actual <- decodeFromPacket (encodeToPacket 0 expected)+        equal "full native greeting" expected actual+    , case2 "capabilities.auth-upper-word-offset" $ do+        let auth = Auth (clientCap .|. bit 35 .|. bit 63) 12345 224 "user" "response" "db" "mysql_native_password"+            packet = encodeToPacket 1 auth+        equal "extension bytes 28..31" [8,0,0,128] (B.unpack (L.toStrict (L.take 4 (L.drop 28 (pBody packet)))))+        equal "auth roundtrip" auth =<< decodeFromPacket packet+    , case2 "capabilities.ssl-upper-word-offset" $ do+        let request = SSLRequest (clientCap .|. bit 35 .|. bit 63 .|. 0x800) 12345 224+            packet = encodeToPacket 1 request+        equal "SSL request length" 32 (pLen packet)+        equal "SSL extension bytes" [8,0,0,128] (B.unpack (L.toStrict (L.drop 28 (pBody packet))))+        equal "SSL roundtrip" request =<< decodeFromPacket packet+    , case2 "capabilities.server-intersection" $ do+        let g = (greeting "mysql_native_password" (B.replicate 20 65)) {greetingCaps=0x8200}+        equal "no unoffered capabilities" 0x8200 (C.negotiatedCapabilities defaultConnectionOptions g)+        assert (C.negotiatedCapabilities defaultConnectionOptions g .&. 0xffffffff00000001 == 0)+            "unimplemented extensions or legacy marker advertised"+    , case2 "auth.ed25519-independent-golden" $ do+        let expected = B.pack [232,61,201,63,67,63,51,53,86,73,238,35,170,117,146,214,26,17,35,9,8,132,245,141,48,99,66,58,36,228,48,84,115,254,187,168,88,162,249,57,35,85,79,238,167,106,68,117,56,135,171,47,20,14,133,79,15,229,124,160,176,100,138,14]+        equal "MariaDB ed25519-t.c vector" expected (C.scrambleEd25519 (B.replicate 32 65) "foobar")+    , case2 "auth.binary-trailing-zero-switch" $ do+        let nonce = B.pack ([1..31] ++ [0])+        (writes, action) <- conversation (greeting "mysql_native_password" (B.replicate 20 65))+            (switch 2 "client_ed25519" nonce) [ok 4]+        action+        sent <- readIORef writes+        equal "signature over all 32 bytes" [L.fromStrict (C.scrambleEd25519 nonce "foobar")] (map pBody sent)+    , case2 "auth.optional-switch-terminator" $ do+        let nonce = B.replicate 20 65+        (writes, action) <- conversation (greeting "mysql_native_password" nonce)+            (switch 2 "caching_sha2_password" (nonce <> "\0")) [ok 4]+        action+        sent <- readIORef writes+        equal "exact nonce without protocol terminator" [L.fromStrict (C.scrambleSHA256 nonce "foobar")] (map pBody sent)+    , case2 "auth.unknown-switch-before-write" $ do+        (writes, action) <- conversation (greeting "mysql_native_password" (B.replicate 20 65))+            (switch 2 "unknown_plugin" (B.replicate 20 65)) []+        rejectAuth action+        equal "no credential response" [] =<< readIORef writes+    , case2 "auth.short-challenges-rejected" $+        forM_ ["mysql_native_password","caching_sha2_password","client_ed25519","parsec"] $ \plugin ->+            forM_ [0,1,19,33,255] $ \len ->+                rejectAuth (evaluate (B.length (C.scrambleForPlugin plugin (B.replicate len 65) "foobar")))+    , case2 "auth.malformed-switch-rejected" $+        forM_ ["\254", "\254missing-nul", "\254\0"] $ \body -> do+            (_, action) <- conversation (greeting "mysql_native_password" (B.replicate 20 65))+                (Packet (fromIntegral (B.length body)) 2 (L.fromStrict body)) []+            rejectAuth action+    , case2 "auth.switch-loop-bounded" $ do+        let nonce = B.replicate 20 65+            first = switch 2 "mysql_native_password" nonce+            rest = [switch n "mysql_native_password" nonce | n <- [4,6..36]]+        (writes, action) <- conversation (greeting "mysql_native_password" nonce) first rest+        rejectAuth action+        sent <- readIORef writes+        assert (length sent <= 16) "auth loop was not bounded"+    , case2 "auth.parsec-excess-work-before-derivation" $ do+        let nonce = B.replicate 32 65+            ext = putToPacket 4 (putByteString (B.pack [1,80,255] <> B.replicate 18 65))+        (writes, action) <- conversation (greeting "mysql_native_password" (B.replicate 20 65))+            (switch 2 "parsec" nonce) [ext]+        rejectAuth action+        equal "only empty ext-salt request" [L.empty] . map pBody =<< readIORef writes+    , case2 "auth.parsec-invalid-parameters" $+        forM_ [B.empty, "P", B.pack [81,0] <> B.replicate 18 65, B.pack [80,0] <> B.replicate 17 65] $ \params -> do+            let ext = putToPacket 4 (putWord8 1 >> putByteString params)+            (_, action) <- conversation (greeting "mysql_native_password" (B.replicate 20 65))+                (switch 2 "parsec" (B.replicate 32 65)) [ext]+            rejectAuth action+    , case2 "tls.sni-and-verified-hostname" $ mapM_ checkSNI [True,False]+    , case2 "auth.sha2-malformed-continuations" $+        forM_ [B.empty, B.pack [3,0], B.pack [4,0], B.singleton 255] $ \body -> do+            let initial=greeting "caching_sha2_password" (B.replicate 20 65)+                packet=putToPacket 2 (putWord8 1 >> putByteString body)+            (writes, action) <- conversation initial packet []+            rejectAuth action+            equal "no malformed continuation response" [] =<< readIORef writes+    , case2 "auth.rsa-invalid-nonce" $+        forM_ [0,1,19,21] $ \count ->+            rejectAuth (evaluate (B.length (C.xorPasswordNonce "foobar" (B.replicate count 65))))+    , case2 "auth.rsa-invalid-key-before-write" $ do+        writes <- newIORef (0::Int)+        rejectAuth (C.doRSAAuth (\_ -> modifyIORef' writes (+1)) 1 "foobar" (B.replicate 20 65) "not PEM")+        equal "invalid key never writes credentials" 0 =<< readIORef writes+    , case2 "auth.rsa-invalid-key-structure" $+        -- Deliberately invalid public SPKI fixtures: negative/zero modulus and+        -- negative/even exponent and an unrelated algorithm identifier.+        -- Reject them before integer conversion or credential encryption.+        forM_ [ "MBwwDQYJKoZIhvcNAQEBBQADCwAwCAIB/wIDAQAB"+              , "MBwwDQYJKoZIhvcNAQEBBQADCwAwCAIBAAIDAQAB"+              , "MBowDQYJKoZIhvcNAQEBBQADCQAwBgIBAQIB/w=="+              , "MBowDQYJKoZIhvcNAQEBBQADCQAwBgIBAQIBBA=="+              , "MBowDQYJKoZIhvcNAQECBQADCQAwBgIBAQIBAw=="+              ] $ \encoded -> do+            let pem="-----BEGIN PUBLIC KEY-----\n" <> encoded <> "\n-----END PUBLIC KEY-----\n"+            case C.parseRSAPublicKey pem of+                Left _ -> pure ()+                Right _ -> throwIO (userError "invalid RSA key structure accepted")+            rejectAuth (C.doRSAAuth (const (pure ())) 1 "foobar" (B.replicate 20 65) pem)+    , case2 "auth.sha2-uncached-full-authentication" $ do+        -- New accounts cannot have been primed by native-client preflight or+        -- another transport. Exercise the full exchange once per transport.+        ci <- config+        withConnect (connect ci {ciUser="testMySQLHaskell",ciPassword="",ciDatabase=""}) $ \admin ->+            forM_ ["tcp","tls","socket"] $ \transport -> do+                stamp <- getMonotonicTimeNSec+                let name="mb_stage2_uncached_" ++ show stamp+                    password="stage2-disposable-full-auth-password"+                    create="CREATE USER '" ++ name ++ "'@'localhost' IDENTIFIED VIA caching_sha2_password USING PASSWORD('" ++ password ++ "')"+                    dropUser="DROP USER '" ++ name ++ "'@'localhost'"+                    account=ci {ciUser=BC.pack name,ciPassword=BC.pack password,ciDatabase=""}+                bracket_ (exec admin create) (exec admin dropUser) $ do+                    opener <- case transport of+                        "tcp" -> pure (connect account)+                        "tls" -> pure (tlsConnect account "localhost")+                        _ -> do path <- socketPath; pure (connectUnixSocket path account)+                    withConnect opener healthy+    , case2 "connection.greeting-deadline-closes-peer" $ withListener $ \listener port -> do+        peerClosed <- newEmptyMVar+        worker <- forkFinally+            (bracket (fst <$> N.accept listener) N.close $ \sock -> NB.recv sock 1)+            (putMVar peerClosed)+        flip finally (killThread worker) $ do+            ci <- config+            rejectAuth (connectWithOptions defaultConnectionOptions {connectTimeoutMicros=50000}+                ci {ciHost="127.0.0.1",ciPort=fromIntegral port})+            result <- timeout 1000000 (takeMVar peerClosed)+            case result of+                Just (Right bytes) -> equal "peer observes cleanup" B.empty bytes+                _ -> throwIO (userError "timed-out handshake did not close its peer")+    , case2 "connection.greeting-cancellation-closes-peer" $ withListener $ \listener port -> do+        accepted <- newEmptyMVar+        peerClosed <- newEmptyMVar+        server <- forkFinally (bracket (fst <$> N.accept listener) N.close $ \sock ->+            putMVar accepted () >> NB.recv sock 1) (putMVar peerClosed)+        ci <- config+        result <- newEmptyMVar+        client <- forkFinally (connect ci {ciHost="127.0.0.1",ciPort=fromIntegral port}) (putMVar result)+        flip finally (killThread client >> killThread server) $ do+            takeMVar accepted+            throwTo client ThreadKilled+            outcome <- takeMVar result+            case outcome of+                Left err -> equal "cancellation identity" (Just ThreadKilled) (fromException err)+                Right conn -> close conn >> throwIO (userError "cancelled constructor succeeded")+            ended <- timeout 1000000 (takeMVar peerClosed)+            case ended of+                Just (Right bytes) -> equal "cancelled peer closed" B.empty bytes+                _ -> throwIO (userError "cancelled handshake leaked transport")+    ] +++    [ case2 ("auth.wrong-password." ++ user ++ "." ++ transport) $ do+        ci <- config+        let wrong = ci {ciUser=BC.pack user,ciPassword="definitely-wrong-stage2-password"}+        opener <- case transport of+            "tcp" -> pure (connect wrong)+            "tls" -> pure (tlsConnect wrong "localhost")+            _ -> do path <- socketPath; pure (connectUnixSocket path wrong)+        expectERR [1045] (withConnect opener healthy)+    | user <- ["mariadb_ed25519","mariadb_parsec","mariadb_sha2"]+    , transport <- ["tcp","tls","socket"]+    ]
+ validation/hs/Validation/Stage3.hs view
@@ -0,0 +1,609 @@+{-# LANGUAGE ScopedTypeVariables #-}+-- | Independent wire fixtures and real-server regressions for native statements.+-- Synthetic packets deliberately use literal protocol bytes where the encoder+-- itself is under test; live cases verify stateful behavior against MariaDB.+module Validation.Stage3 (cases, bulkFeature, cacheFeature, extendedFeature) where++import Control.Concurrent+import Control.Exception hiding (assert)+import Control.Monad+import Data.Binary.Put+import Data.Bits+import qualified Data.ByteString as B+import qualified Data.ByteString.Internal as BI+import qualified Data.ByteString.Lazy as L+import Data.IORef+import Data.List (isInfixOf)+import qualified Data.IntMap.Strict as IM+import Data.Time+import Data.Scientific (scientific)+import qualified Data.Text as T+import Data.Word+import Database.MySQL.Base+import qualified Database.MySQL.Connection as C+import qualified System.IO.Streams as S+import System.Timeout+import Validation.Common+import Validation.FaultTransport (scripted, withFaultProxy, ProxyMode(..), ProxyStats(..))++case3 :: String -> IO () -> Case+case3 name = Case ("stage3." ++ name) "stage3"++field :: ColumnDef+field = ColumnDef "" "" "" "n" "" 63 20 mySQLTypeLongLong 0 0++preparePackets :: Word32 -> [Packet]+preparePackets ident = [putToPacket 1 (putWord8 0 >> putWord32le ident >>+    putWord16le 1 >> putWord16le 0 >> putWord8 0 >> putWord16le 0), encodeToPacket 2 field]++end :: Word8 -> Word16 -> Packet+end sequenceNumber flags = putToPacket sequenceNumber $ do+    putWord8 254; putWord8 0; putWord8 0; putWord16le flags; putWord16le 0++cachedRows :: Integer -> [Packet]+cachedRows value = [putToPacket 1 (putWord8 1 >> putWord8 0),+    putToPacket 2 (putWord8 0 >> putWord8 0 >> putWord64le (fromIntegral value)), end 3 2]++cachedFixture :: [Packet] -> IO (MySQLConn, IO [Packet], IO Int)+cachedFixture packets = do+    result@(conn, _, _) <- scripted defaultConnectionOptions packets+    writeIORef (C.connectionCapabilities conn) (bit 24 .|. bit 36)+    pure result++allPreparedRows :: [PreparedResult] -> [[MySQLValue]]+allPreparedRows results = concat [values | PreparedRows _ values <- results]++-- A large, entirely buffered result never blocks on a socket/MVar. It exposes+-- deadlines/cancellation accidentally masked by a bracket's cleanup phase.+-- The finite tail also bounds a broken implementation during development.+bufferedFixture :: Int -> [Packet] -> IO (MySQLConn, IO Int, MVar (), IO Int)+bufferedFixture cleanupMicros prefix = do+    packets <- newIORef prefix+    consumed <- newIORef 0+    started <- newEmptyMVar+    released <- newIORef 0+    input <- S.makeInputStream $ do+        next <- atomicModifyIORef' packets (\xs -> case xs of [] -> ([], Nothing); x:rest -> (rest, Just x))+        case next of+            Just packet -> pure (Just packet)+            Nothing -> do+                index <- atomicModifyIORef' consumed (\n -> (n + 1, n))+                void (tryPutMVar started ())+                pure (Just (if index < 10000000+                    then putToPacket (fromIntegral (index + 2)) (putWord8 0 >> putWord8 0 >> putWord64le 7)+                    else end (fromIntegral (index + 2)) 2))+    conn <- C.newConnection defaultConnectionOptions {cleanupTimeoutMicros=cleanupMicros}+        input (const (pure ())) (modifyIORef' released (+1))+    writeIORef (C.connectionCapabilities conn) (bit 24 .|. bit 36)+    pure (conn, readIORef consumed, started, readIORef released)++bulkFeature :: IO ()+bulkFeature = withConn $ \conn -> do+    exec conn "CREATE TEMPORARY TABLE mb_stage3_bulk(n BIGINT, v VARCHAR(100))"+    withPreparedStatement conn "INSERT INTO mb_stage3_bulk VALUES(?,?)" $ \stmt -> do+        ok <- executeBulk stmt [[BulkValue (MySQLInt64 n), BulkValue (MySQLText "native")] | n <- [1..3]]+        equal "aggregate affected rows" 3 (okAffectedRows ok)+    equal "bulk content" [[3,6]] . map (map number) =<< rows conn "SELECT COUNT(*),SUM(n) FROM mb_stage3_bulk"+    healthy conn++cacheFeature :: IO ()+cacheFeature = withConn $ \conn -> withPreparedStatement conn "SELECT CAST(? AS SIGNED) AS n" $ \stmt -> do+    caps <- readIORef (C.connectionCapabilities conn)+    assert (testBit caps 36) "metadata caching was not negotiated"+    forM_ [7,8,9] $ \value -> do+        (_, stream) <- queryPrepared stmt [MySQLInt64 value]+        equal "repeated cached result" [[fromIntegral value]] . map (map number) =<< S.toList stream+    healthy conn++extendedFeature :: IO ()+extendedFeature = withConn $ \conn -> do+    exec conn "CREATE TEMPORARY TABLE mb_stage3_native(u UUID,j JSON,ip INET6,v VECTOR(3))"+    withPreparedStatement conn "SELECT u,j,ip,v FROM mb_stage3_native" $ \stmt -> do+        metadata <- preparedMetadata stmt+        equal "native column names" ["u","j","ip","v"] (map (columnName . metadataColumn) metadata)+        let attributes = concatMap metadataAttributes metadata+        assert (any ((=="uuid") . snd) attributes) "UUID native metadata missing"+        assert (any ((=="json") . snd) attributes) "JSON format metadata missing"+        (_, stream) <- queryPrepared stmt []+        equal "empty native result" [] =<< S.toList stream++cases :: [Case]+cases =+    [ case3 "prepared.out-and-multiple-results" $ withConn $ \conn -> do+        exec conn "DROP PROCEDURE IF EXISTS mb_stage3_out"+        exec conn "CREATE PROCEDURE mb_stage3_out(IN a INT,INOUT b INT,OUT c INT) BEGIN SELECT a; SELECT a+1; SET b=b+2; SET c=a+b; END"+        withPreparedStatement conn "CALL mb_stage3_out(?,?,?)" $ \stmt -> do+            result <- queryPreparedMulti stmt [MySQLInt32 7,MySQLInt32 10,MySQLNull]+            equal "ordinary result sets" [[7],[8]] (map (map number) (allPreparedRows result))+            equal "OUT values distinct from ordinary rows" [[12,19]]+                [map number values | PreparedOutParameters _ values <- result]+            assert (any isOKResult result) "trailing procedure OK missing"+        healthy conn+        exec conn "DROP PROCEDURE mb_stage3_out"+    , case3 "prepared.raw-multiple-results" $ withConn $ \conn -> do+        exec conn "DROP PROCEDURE IF EXISTS mb_stage3_multi"+        exec conn "CREATE PROCEDURE mb_stage3_multi() BEGIN SELECT 7; SELECT 8; END"+        bracket (prepareStmt conn "CALL mb_stage3_multi()") (closeStmt conn) $ \ident -> do+            result <- queryStmtMulti conn ident []+            equal "raw binary multiple results" [[7],[8]] (map (map number) (allPreparedRows result))+        healthy conn+        exec conn "DROP PROCEDURE mb_stage3_multi"+    , case3 "prepared.single-result-drains-extras" $ withConn $ \conn -> do+        exec conn "DROP PROCEDURE IF EXISTS mb_stage3_extra"+        exec conn "CREATE PROCEDURE mb_stage3_extra() BEGIN SELECT 7; SELECT 8; END"+        withPreparedStatement conn "CALL mb_stage3_extra()" $ \stmt -> do+            result <- try (queryPrepared stmt [] >>= S.toList . snd)+            case result of Left ExtraResultSets -> pure (); _ -> fail "extra rows were silently lost"+        healthy conn+        exec conn "DROP PROCEDURE mb_stage3_extra"+    , case3 "prepared.local-arity" $ do+        (conn, sent, _) <- cachedFixture (preparePackets 7)+        stmt <- prepareStatement conn "SELECT 7"+        before <- length <$> sent+        expectRejection (queryPrepared stmt [MySQLInt32 1])+        equal "arity rejection sends nothing" before . length =<< sent+        equal "arity preserves readiness" Ready =<< connectionStatus conn+    , case3 "prepared.close-idempotent" $ do+        (conn, sent, _) <- cachedFixture (preparePackets 7)+        stmt <- prepareStatement conn "SELECT 7"+        closePrepared stmt+        before <- length <$> sent+        closePrepared stmt+        expectRejection (queryPrepared stmt [])+        equal "closed handle sends nothing" before . length =<< sent+    , case3 "prepared.raw-id-reuse" $ do+        (conn, sent, _) <- cachedFixture (preparePackets 7 ++ preparePackets 7 ++ cachedRows 8)+        old <- prepareStatement conn "SELECT 7"+        closeStmt conn 7+        new <- prepareStatement conn "SELECT 8"+        before <- length <$> sent+        closePrepared old+        expectRejection (queryPrepared old [])+        equal "old generation cannot close replacement" before . length =<< sent+        equal "replacement still executes" [[8]] . map (map number) =<< (queryPrepared new [] >>= S.toList . snd)+        closePrepared new+    , case3 "prepared.registry-bound" $ do+        ci <- config+        withConnect (connectWithOptions defaultConnectionOptions {maximumPreparedStatements=1} ci) $ \conn -> do+            stmt <- prepareStatement conn "SELECT 7"+            expectRejection (prepareStatement conn "SELECT 8")+            healthy conn+            closePrepared stmt+            withPreparedStatement conn "SELECT 9" $ \next ->+                equal "released registry slot" [[9]] . map (map number) =<< (queryPrepared next [] >>= S.toList . snd)+    , case3 "prepared.pending-close-rejected" $ withConn $ \conn -> do+        stmt <- prepareStatement conn "SELECT seq FROM seq_1_to_10"+        (_, stream) <- queryPrepared stmt []+        void (S.read stream)+        expectRejection (closePrepared stmt)+        S.skipToEof stream+        closePrepared stmt+        healthy conn+    , case3 "prepared.bracket-preserves-primary" $ withConn $ \conn -> do+        outcome <- try $ withPreparedStatement conn "SELECT seq FROM seq_1_to_10" $ \stmt -> do+            (_, stream) <- queryPrepared stmt []+            void (S.read stream)+            throwIO (userError "stage3-primary-marker")+        case outcome of+            Left (err :: SomeException) -> assert ("stage3-primary-marker" `isInfixOf` displayException err) "cleanup replaced primary exception"+            Right () -> fail "body exception was lost"+        healthy conn+    , case3 "prepared.reset-retains-cache" $ withConn $ \conn -> withPreparedStatement conn "SELECT ?" $ \stmt -> do+        equal "initial execution" [[7]] . map (map number) =<< (queryPrepared stmt [MySQLInt32 7] >>= S.toList . snd)+        resetPrepared stmt+        equal "execution after statement reset" [[8]] . map (map number) =<< (queryPrepared stmt [MySQLInt32 8] >>= S.toList . snd)+    , case3 "prepared.connection-close-invalidates" $ withConn $ \conn -> do+        stmt <- prepareStatement conn "SELECT 7"+        close conn+        closePrepared stmt+        expectRejection (preparedMetadata stmt)+        expectRejection (queryPrepared stmt [])+    , case3 "timeout.safe-retirement-default" $ withConn $ \conn -> do+        result <- timeout 20000 (rows conn "SELECT SLEEP(0.2)")+        equal "deadline propagated" Nothing result+        equal "unsafe session retired" Broken =<< connectionStatus conn+        expectRejection (ping conn)+        withConn healthy+    , case3 "cache.omitted-first-execution" $ do+        (conn, _, _) <- cachedFixture (preparePackets 7 ++ cachedRows 42)+        stmt <- prepareStatement conn "SELECT 42"+        equal "prepare metadata decodes omitted execute metadata" [[42]] . map (map number) =<< (queryPrepared stmt [] >>= S.toList . snd)+    , case3 "cache.missing-entry-retires" $ do+        (conn, _, released) <- cachedFixture (cachedRows 42)+        expectRejection (queryStmt conn 7 [])+        equal "missing cache is terminal" Broken =<< connectionStatus conn+        equal "transport released" 1 =<< released+    , case3 "cache.invalid-marker" $ do+        (conn, _, _) <- cachedFixture (preparePackets 7 ++ [putToPacket 1 (putWord8 1 >> putWord8 2)])+        stmt <- prepareStatement conn "SELECT 7"+        expectRejection (queryPrepared stmt [])+        equal "invalid marker retires" Broken =<< connectionStatus conn+    , case3 "cache.count-mismatch" $ do+        (conn, _, _) <- cachedFixture (preparePackets 7 ++ [putToPacket 1 (putWord8 2 >> putWord8 0)])+        stmt <- prepareStatement conn "SELECT 7"+        expectRejection (queryPrepared stmt [])+        equal "omitted changed shape rejected" Broken =<< connectionStatus conn+    , case3 "cache.ddl-reprepare" $ withConn $ \conn -> do+        exec conn "CREATE TEMPORARY TABLE mb_stage3_ddl(n INT)"+        exec conn "INSERT INTO mb_stage3_ddl VALUES(7)"+        withPreparedStatement conn "SELECT * FROM mb_stage3_ddl" $ \stmt -> do+            equal "initial schema" [[7]] . map (map number) =<< (queryPrepared stmt [] >>= S.toList . snd)+            exec conn "ALTER TABLE mb_stage3_ddl ADD v VARCHAR(20) DEFAULT 'changed'"+            (columns, stream) <- queryPrepared stmt []+            equal "changed result shape" ["n","v"] (map columnName columns)+            values <- S.toList stream+            equal "updated decoder" [[MySQLInt32 7, MySQLText "changed"]] values+            equal "replacement cached" values =<< (queryPrepared stmt [] >>= S.toList . snd)+    , case3 "cache.parameter-type-refresh" $ withConn $ \conn -> withPreparedStatement conn "SELECT ? AS n" $ \stmt -> do+        forM_ [MySQLInt32 7,MySQLText "changed",MySQLInt64U maxBound,MySQLNull] $ \value -> do+            result <- queryPrepared stmt [value] >>= S.toList . snd+            case value of+                MySQLNull -> equal "NULL type change" [[MySQLNull]] result+                MySQLText text -> equal "text type change" [[MySQLText text]] result+                _ -> equal "numeric type change" [[number value]] (map (map number) result)+    , case3 "cache.explicit-opt-out" $ do+        ci <- config+        withConnect (connectWithOptions defaultConnectionOptions {enableMetadataCaching=False,enableExtendedMetadata=False} ci) $ \conn -> do+            caps <- readIORef (C.connectionCapabilities conn)+            assert (not (testBit caps 36 || testBit caps 35)) "opted-out flags advertised"+            withPreparedStatement conn "SELECT ?" $ \stmt -> forM_ [7,8] $ \value ->+                equal "uncached binary result" [[fromIntegral value]] . map (map number) =<< (queryPrepared stmt [MySQLInt32 value] >>= S.toList . snd)+    , case3 "cache.legacy-eof" $ do+        ci <- config+        withConnect (connectWithOptions defaultConnectionOptions {enableSessionTracking=False} ci) $ \conn ->+            withPreparedStatement conn "SELECT 7" $ \stmt -> replicateM_ 3 $+                equal "legacy EOF with omission" [[7]] . map (map number) =<< (queryPrepared stmt [] >>= S.toList . snd)+    , case3 "cache.aggregate-budget" $ do+        let bytes = fromIntegral (pLen (encodeToPacket 2 field))+        (conn, _, released) <- scripted defaultConnectionOptions {maximumMetadataBytes=bytes+1}+            (preparePackets 7 ++ preparePackets 8)+        writeIORef (C.connectionCapabilities conn) (bit 24 .|. bit 36)+        first <- prepareStatement conn "SELECT 7"+        expectRejection (prepareStatement conn "SELECT 8")+        equal "aggregate cache budget retires safely" Broken =<< connectionStatus conn+        equal "budget failure releases transport" 1 =<< released+        expectRejection (preparedMetadata first)+    , case3 "metadata.unknown-attribute" $ do+        let packet = putToPacket 0 $ do+                mapM_ putLenEncBytes ["def","","","","n",""]+                putLenEncBytes (B.pack [42,3,97,0,98])+                putWord8 12; putWord16le 63; putWord32le 20+                putWord8 8; putWord16le 0; putWord8 0; putWord16le 0+        actual <- getFromPacketExact (getColumnMetadata True) packet+        equal "unknown tag and embedded NUL retained" [(42,B.pack [97,0,98])] (metadataAttributes actual)+        equal "core metadata not shifted" field (metadataColumn actual)+    , case3 "metadata.truncated-attribute" $ do+        let packet = putToPacket 0 $ do+                mapM_ putLenEncBytes ["def","","","","n",""]+                putLenEncBytes (B.pack [0,5,97])+                putWord8 12; putWord16le 63; putWord32le 20+                putWord8 8; putWord16le 0; putWord8 0; putWord16le 0+        expectRejection (getFromPacketExact (getColumnMetadata True) packet)+    , case3 "bulk.independent-wire-vector" $ do+        let cmd = COM_STMT_BULK_EXECUTE 0x01020304 128 [MySQLInt32 7,MySQLText "a"]+                [[BulkValue (MySQLInt32 7),BulkValue (MySQLText "a")], [BulkValue MySQLNull,BulkDefault]]+        equal "literal bulk wire bytes" [250,4,3,2,1,128,0,3,0,254,0,0,7,0,0,0,0,1,97,1,2]+            (L.unpack (runPut (putCommand cmd)))+    , case3 "bulk.per-unit-results" $ withConn $ \conn -> do+        exec conn "CREATE TEMPORARY TABLE mb_stage3_units(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,n INT)"+        withPreparedStatement conn "INSERT INTO mb_stage3_units(n) VALUES(?)" $ \stmt -> do+            result <- executeBulkUnitResults stmt [[BulkValue (MySQLInt32 n)] | n <- [7,8,9]]+            equal "individual IDs and counts" [BulkUnitResult 1 1,BulkUnitResult 2 1,BulkUnitResult 3 1] result+        healthy conn+    , case3 "bulk.null-default-ignore" $ withConn $ \conn -> do+        exec conn "CREATE TEMPORARY TABLE mb_stage3_indicators(n INT DEFAULT 7,v VARCHAR(20) DEFAULT 'default')"+        withPreparedStatement conn "INSERT INTO mb_stage3_indicators VALUES(?,?)" $ \stmt -> do+            void (executeBulk stmt [[BulkDefault,BulkDefault],[BulkIgnore,BulkValue MySQLNull],+                [BulkValue (MySQLInt32 9),BulkValue (MySQLText "value")]])+        equal "native indicators" [[MySQLInt32 7,MySQLNull],[MySQLInt32 7,MySQLText "default"],[MySQLInt32 9,MySQLText "value"]]+            =<< rows conn "SELECT n,v FROM mb_stage3_indicators ORDER BY n,v"+    , case3 "bulk.update-ignore" $ withConn $ \conn -> do+        exec conn "CREATE TEMPORARY TABLE mb_stage3_update(id INT PRIMARY KEY,n INT DEFAULT 7)"+        exec conn "INSERT INTO mb_stage3_update VALUES(1,10),(2,20)"+        withPreparedStatement conn "UPDATE mb_stage3_update SET n=? WHERE id=?" $ \stmt ->+            void (executeBulk stmt [[BulkIgnore,BulkValue (MySQLInt32 1)],[BulkDefault,BulkValue (MySQLInt32 2)]])+        equal "UPDATE indicators" [[1,10],[2,7]] . map (map number) =<< rows conn "SELECT id,n FROM mb_stage3_update ORDER BY id"+    , case3 "bulk.transaction-rollback-no-replay" $ withConn $ \conn -> do+        exec conn "CREATE TEMPORARY TABLE mb_stage3_atomic(n INT PRIMARY KEY) ENGINE=InnoDB"+        withPreparedStatement conn "INSERT INTO mb_stage3_atomic VALUES(?)" $ \stmt -> do+            expectERR [1062] $ withTransaction conn $ void $ executeBulk stmt+                [[BulkValue (MySQLInt32 1)],[BulkValue (MySQLInt32 1)],[BulkValue (MySQLInt32 2)]]+        equal "explicit transaction rolled back" 0 =<< scalarInt conn "SELECT COUNT(*) FROM mb_stage3_atomic"+        healthy conn+    , case3 "bulk.mixed-types-rejected" $ withConn $ \conn -> do+        exec conn "CREATE TEMPORARY TABLE mb_stage3_types(n BIGINT)"+        withPreparedStatement conn "INSERT INTO mb_stage3_types VALUES(?)" $ \stmt -> do+            expectRejection (executeBulk stmt [[BulkValue (MySQLInt32 1)],[BulkValue (MySQLInt64 2)]])+            expectRejection (executeBulk stmt [[BulkValue (MySQLInt32 1)],[BulkValue (MySQLInt32U 2)]])+        equal "type rejection sends no INSERT" 0 =<< scalarInt conn "SELECT COUNT(*) FROM mb_stage3_types"+        healthy conn+    , case3 "bulk.empty-and-arity-rejected" $ withConn $ \conn -> do+        exec conn "CREATE TEMPORARY TABLE mb_stage3_arity(n INT)"+        withPreparedStatement conn "INSERT INTO mb_stage3_arity VALUES(?)" $ \stmt -> do+            expectRejection (executeBulk stmt [])+            expectRejection (executeBulk stmt [[]])+            expectRejection (executeBulk stmt [[BulkValue (MySQLInt32 1),BulkDefault]])+        equal "invalid dimensions send no INSERT" 0 =<< scalarInt conn "SELECT COUNT(*) FROM mb_stage3_arity"+        healthy conn+    , case3 "bulk.row-limit" $ do+        ci <- config+        withConnect (connectWithOptions defaultConnectionOptions {maximumBulkRows=2} ci) $ \conn -> do+            exec conn "CREATE TEMPORARY TABLE mb_stage3_limit(n INT)"+            withPreparedStatement conn "INSERT INTO mb_stage3_limit VALUES(?)" $ \stmt ->+                expectRejection (executeBulk stmt [[BulkValue (MySQLInt32 n)] | n <- [1..3]])+            equal "oversized batch not split" 0 =<< scalarInt conn "SELECT COUNT(*) FROM mb_stage3_limit"+            healthy conn+    , case3 "bulk.max-bound-limits" $ do+        ci <- config+        withConnect (connectWithOptions defaultConnectionOptions+            {maximumBulkRows=maxBound, maximumColumns=maxBound} ci) $ \conn -> do+            exec conn "CREATE TEMPORARY TABLE mb_stage3_max_bound(n INT)"+            withPreparedStatement conn "INSERT INTO mb_stage3_max_bound VALUES(?)" $ \stmt -> do+                ok <- executeBulk stmt [[BulkValue (MySQLInt32 7)], [BulkValue (MySQLInt32 8)]]+                equal "permissive bounds accept a small batch" 2 (okAffectedRows ok)+            equal "maxBound batch contents" [[7], [8]] . map (map number)+                =<< rows conn "SELECT n FROM mb_stage3_max_bound ORDER BY n"+            healthy conn+    , case3 "bulk.message-limit" $ do+        ci <- config+        withConnect (connectWithOptions defaultConnectionOptions {maximumMessageBytes=2048} ci) $ \conn -> do+            exec conn "CREATE TEMPORARY TABLE mb_stage3_message(v BLOB)"+            withPreparedStatement conn "INSERT INTO mb_stage3_message VALUES(?)" $ \stmt ->+                expectRejection (executeBulk stmt [[BulkValue (MySQLBytes (B.replicate 4096 65))]])+            equal "oversized message sends no INSERT" 0 =<< scalarInt conn "SELECT COUNT(*) FROM mb_stage3_message"+            healthy conn+    , case3 "bulk.charset-and-unencodable" $ withConn $ \conn -> do+        exec conn "CREATE TEMPORARY TABLE mb_stage3_charset(v VARCHAR(100) CHARACTER SET utf8mb4)"+        exec conn "SET NAMES latin1"+        refreshSessionState conn+        withPreparedStatement conn "INSERT INTO mb_stage3_charset VALUES(?)" $ \stmt -> do+            void (executeBulk stmt [[BulkValue (MySQLText "caf\233")]])+            expectRejection (executeBulk stmt [[BulkValue (MySQLText "\x4f60")]])+        exec conn "SET NAMES utf8mb4"+        equal "bulk honors active client encoding" [[MySQLText "caf\233"]] =<< rows conn "SELECT v FROM mb_stage3_charset"+    , case3 "bulk.sql-looking-values" $ withConn $ \conn -> do+        exec conn "CREATE TEMPORARY TABLE mb_stage3_safe(v TEXT)"+        let value = "\NUL'\\; DROP TABLE mb_stage3_safe; -- \x1f642"+        withPreparedStatement conn "INSERT INTO mb_stage3_safe VALUES(?)" $ \stmt ->+            void (executeBulk stmt [[BulkValue (MySQLText value)]])+        equal "bulk data never parsed as SQL" [[MySQLText value]] =<< rows conn "SELECT v FROM mb_stage3_safe"+    , case3 "bulk.zero-parameter-rejected" $ do+        (conn, sent, _) <- cachedFixture (preparePackets 7)+        stmt <- prepareStatement conn "SELECT 7"+        before <- length <$> sent+        expectRejection (executeBulk stmt [[BulkValue (MySQLInt32 1)]])+        equal "zero-parameter bulk sends nothing" before . length =<< sent+    , case3 "bulk.scalar-null-parity" $ withConn $ \conn -> do+        exec conn "CREATE TEMPORARY TABLE mb_stage3_scalars(id INT,i8 TINYINT,u8 TINYINT UNSIGNED,i16 SMALLINT,u16 SMALLINT UNSIGNED,i32 INT,u32 INT UNSIGNED,i64 BIGINT,u64 BIGINT UNSIGNED,d DECIMAL(30,6),f FLOAT,g DOUBLE,b BIT(64),dt DATETIME(6),da DATE,tm TIME(6),yr YEAR,t TEXT,raw BLOB)"+        let day = fromGregorian 2024 2 29+            values = [MySQLInt8 minBound,MySQLInt8U maxBound,MySQLInt16 minBound,MySQLInt16U maxBound,+                MySQLInt32 minBound,MySQLInt32U maxBound,MySQLInt64 minBound,MySQLInt64U maxBound,+                MySQLDecimal (scientific 12345678901234567890123456 (-6)),MySQLFloat 0.125,MySQLDouble 0.125,+                MySQLBit maxBound,MySQLDateTime (LocalTime day (TimeOfDay 23 59 59.123456)),MySQLDate day,+                MySQLTime 1 (TimeOfDay 838 59 59.123456),MySQLYear 2024,+                MySQLText "\NULunicode \x1f642",MySQLBytes (B.pack [0..255])]+        withPreparedStatement conn "INSERT INTO mb_stage3_scalars VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)" $ \stmt ->+            void (executeBulk stmt [BulkValue (MySQLInt32 1):map BulkValue values,+                BulkValue (MySQLInt32 2):replicate 18 (BulkValue MySQLNull)])+        bracket (prepareStmt conn "SELECT i8,u8,i16,u16,i32,u32,i64,u64,d,f,g,b,dt,da,tm,yr,t,raw FROM mb_stage3_scalars ORDER BY id") (closeStmt conn) $ \ident ->+            equal "bulk/prepared scalar parity including NULL bitmap" [values,replicate 18 MySQLNull]+                =<< (queryStmt conn ident [] >>= S.toList . snd)+    , case3 "bulk.all-null-and-default-column" $ withConn $ \conn -> do+        exec conn "CREATE TEMPORARY TABLE mb_stage3_null(n INT DEFAULT 7)"+        withPreparedStatement conn "INSERT INTO mb_stage3_null VALUES(?)" $ \stmt -> do+            void (executeBulk stmt [[BulkValue MySQLNull],[BulkValue MySQLNull]])+            void (executeBulk stmt [[BulkDefault],[BulkDefault]])+        equal "all-NULL/default type selection" [[4,2,14]] . map (map number)+            =<< rows conn "SELECT COUNT(*),COUNT(n),SUM(n) FROM mb_stage3_null"+    , case3 "bulk.unit-result-unsigned-id" $ withConn $ \conn -> do+        exec conn "CREATE TEMPORARY TABLE mb_stage3_unsigned(id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,n INT) AUTO_INCREMENT=9223372036854775808"+        withPreparedStatement conn "INSERT INTO mb_stage3_unsigned(n) VALUES(?)" $ \stmt -> do+            result <- executeBulkUnitResults stmt [[BulkValue (MySQLInt32 7)],[BulkValue (MySQLInt32 8)]]+            equal "full Word64 generated IDs" [BulkUnitResult 9223372036854775808 1,BulkUnitResult 9223372036854775809 1] result+    , case3 "bulk.physical-boundary" $ withConn $ \conn -> do+        exec conn "CREATE TEMPORARY TABLE mb_stage3_large(v LONGBLOB)"+        let payload = B.replicate 16777216 65+        withPreparedStatement conn "INSERT INTO mb_stage3_large VALUES(?)" $ \stmt ->+            void (executeBulk stmt [[BulkValue (MySQLBytes payload)],[BulkValue (MySQLBytes "small")]])+        equal "bulk continuation content" [[2,16777221]] . map (map number)+            =<< rows conn "SELECT COUNT(*),SUM(OCTET_LENGTH(v)) FROM mb_stage3_large"+        equal "large payload content" [[65,65]] . map (map number)+            =<< rows conn "SELECT ASCII(LEFT(v,1)),ASCII(RIGHT(v,1)) FROM mb_stage3_large WHERE OCTET_LENGTH(v)>100"+        healthy conn+    , case3 "bulk.non-finite-rejected" $ withConn $ \conn -> do+        exec conn "CREATE TEMPORARY TABLE mb_stage3_nan(n DOUBLE)"+        withPreparedStatement conn "INSERT INTO mb_stage3_nan VALUES(?)" $ \stmt -> do+            expectRejection (executeBulk stmt [[BulkValue (MySQLDouble (0/0))]])+            expectRejection (executeBulk stmt [[BulkValue (MySQLDouble (1/0))]])+        equal "invalid values send no INSERT" 0 =<< scalarInt conn "SELECT COUNT(*) FROM mb_stage3_nan"+        healthy conn+    , case3 "bulk.one-wire-command" $ do+        ci <- config+        withFaultProxy ci (FragmentTraffic 65536) $ \proxyCi statistics -> withConnect (connect proxyCi) $ \conn -> do+            exec conn "CREATE TEMPORARY TABLE mb_stage3_wire(n INT)"+            withPreparedStatement conn "INSERT INTO mb_stage3_wire VALUES(?)" $ \stmt -> do+                before <- proxyCommands <$> statistics+                void (executeBulk stmt [[BulkValue (MySQLInt32 n)] | n <- [1..100]])+                after <- proxyCommands <$> statistics+                equal "one native batch command, no hidden helper query" 1 (after-before)+    , case3 "bulk.capability-rejection-before-send" $ do+        (conn, sent, _) <- cachedFixture []+        let cmd = COM_STMT_BULK_EXECUTE 7 128 [MySQLInt32 1] [[BulkValue (MySQLInt32 1)]]+        expectRejection (C.withCommand conn (C.sendCommand conn cmd))+        equal "unsupported bulk flag sends nothing" [] =<< sent+        writeIORef (C.connectionCapabilities conn) (bit 34)+        expectRejection (C.withCommand conn (C.sendCommand conn (COM_STMT_BULK_EXECUTE 7 192 [MySQLInt32 1] [[BulkValue (MySQLInt32 1)]])))+        equal "unsupported unit results send nothing" [] =<< sent+    , case3 "prepared.cleanup-deadline" $ do+        packets <- newIORef (preparePackets 7 ++ [putToPacket 1 (putWord8 1 >> putWord8 0)])+        stalled <- newEmptyMVar+        input <- S.makeInputStream $ do+            next <- atomicModifyIORef' packets (\xs -> case xs of [] -> ([],Nothing); x:rest -> (rest,Just x))+            maybe (takeMVar stalled) (pure . Just) next+        conn <- C.newConnection defaultConnectionOptions {cleanupTimeoutMicros=20000} input (const (pure ())) (pure ())+        writeIORef (C.connectionCapabilities conn) (bit 24 .|. bit 36)+        outcome <- timeout 1000000 $ try $ withPreparedStatement conn "SELECT 7" $ \stmt -> do+            void (queryPrepared stmt [])+            throwIO (userError "stage3-bounded-primary")+        case outcome of+            Just (Left (err :: SomeException)) -> assert ("stage3-bounded-primary" `isInfixOf` displayException err) "cleanup did not preserve primary"+            _ -> fail "statement cleanup exceeded deadline"+        equal "stalled cleanup retires" Broken =<< connectionStatus conn+    , case3 "prepared.buffered-cleanup-deadline" $ do+        (conn, consumed, _, released) <- bufferedFixture 20000+            (preparePackets 7 ++ [putToPacket 1 (putWord8 1 >> putWord8 0)])+        result <- try $ withPreparedStatement conn "SELECT 7" $ \stmt -> void (queryPrepared stmt [])+        case result of+            Left CleanupDeadlineExceeded -> pure ()+            _ -> fail "buffered drain ignored its cleanup deadline"+        count <- consumed+        assert (count < 10000000) "cleanup consumed the entire oversized result"+        equal "buffered timeout retires" Broken =<< connectionStatus conn+        equal "buffered timeout releases once" 1 =<< released+    , case3 "prepared.buffered-cleanup-cancellation" $ do+        (conn, _, started, released) <- bufferedFixture 5000000+            (preparePackets 7 ++ [putToPacket 1 (putWord8 1 >> putWord8 0)])+        done <- newEmptyMVar+        worker <- forkFinally+            (withPreparedStatement conn "SELECT 7" (\stmt -> void (queryPrepared stmt [])))+            (putMVar done)+        takeMVar started+        killThread worker+        outcome <- takeMVar done+        case outcome of+            Left err | Just ThreadKilled <- fromException err -> pure ()+            _ -> fail "buffered cleanup swallowed the new cancellation"+        equal "cancelled buffered cleanup retires" Broken =<< connectionStatus conn+        equal "cancelled buffered cleanup releases once" 1 =<< released+    , case3 "transaction.buffered-cleanup-deadline" $ do+        (conn, consumed, _, released) <- bufferedFixture 20000+            (preparePackets 7 ++ [encodeToPacket 1 (OK 0 0 3 0), putToPacket 1 (putWord8 1 >> putWord8 0)])+        writeIORef (C.connectionServerStatus conn) (Just 2)+        outcome <- try $ withPreparedStatement conn "SELECT 7" $ \stmt ->+            withTransaction conn (void (queryPrepared stmt []))+        case outcome of+            Left UnconsumedResultSet -> pure ()+            _ -> fail "transaction cleanup replaced the primary pending-result error"+        count <- consumed+        assert (count < 10000000) "rollback cleanup ignored its deadline on buffered data"+        equal "timed-out rollback drain retires" Broken =<< connectionStatus conn+        equal "rollback cleanup releases once" 1 =<< released+    , case3 "cache.incremental-accounting" $ do+        (conn, _, _) <- scripted defaultConnectionOptions {maximumMetadataBytes=100} []+        let block bytes = C.CachedColumns [] [] 0 (pure []) bytes+            store ident ordinal bytes = void (C.storeStatementColumns conn ident ordinal (block bytes))+            charge expected = do+                registry <- readIORef (C.connectionStatements conn)+                case registry of+                    Nothing -> fail "unexpected terminal registry"+                    Just current -> do+                        equal "aggregate metadata charge" expected (C.registryMetadataBytes current)+                        let states = IM.elems (C.registryStatements current)+                        equal "per-statement totals agree" expected (sum (map C.statementMetadataBytes states))+                        equal "cached blocks agree" expected+                            (sum [C.cachedMetadataBytes columns | state <- states, columns <- IM.elems (C.statementResults state)])+        C.registerStatement conn 7 1 0+        store 7 0 10+        charge 10+        store 7 0 25+        store 7 1 20+        charge 45+        C.registerStatement conn 8 2 0+        store 8 0 30+        charge 75+        C.forgetStatement conn 7+        C.forgetStatement conn 7+        charge 30+        C.registerStatement conn 8 3 0+        charge 0+        store 8 0 100+        expectRejection (store 8 1 1)+        charge 100+        close conn+        equal "terminal close discards accounting" Nothing . fmap C.registryMetadataBytes+            =<< readIORef (C.connectionStatements conn)+    , case3 "cache.close-releases-budget" $ do+        let bytes = fromIntegral (pLen (encodeToPacket 2 field))+        (conn, _, _) <- scripted defaultConnectionOptions {maximumMetadataBytes=bytes}+            (preparePackets 7 ++ preparePackets 8)+        writeIORef (C.connectionCapabilities conn) (bit 24 .|. bit 36)+        first <- prepareStatement conn "SELECT 7"+        closePrepared first+        second <- prepareStatement conn "SELECT 8"+        equal "released budget admits replacement" 1 . length =<< preparedMetadata second+        closePrepared second+    , case3 "cache.close-race-does-not-resurrect" $ do+        (conn, _, _) <- cachedFixture (preparePackets 7)+        stmt <- prepareStatement conn "SELECT 7"+        close conn+        empty <- C.statementMap conn+        equal "terminal registry empty" 0 (length empty)+        expectRejection (C.storeStatementColumns conn 7 0 (C.CachedColumns [] [] 0 (pure []) 0))+        expectRejection (preparedMetadata stmt)+    , case3 "cache.server-confirms-omission" $ withConn $ \conn -> do+        let skipped = do+                status <- rows conn "SHOW SESSION STATUS LIKE 'Resultset_metadata_skipped'"+                case status of+                    [[_,MySQLText value]] -> pure (read (T.unpack value) :: Integer)+                    _ -> fail "MariaDB metadata omission counter unavailable"+        withPreparedStatement conn "SELECT 7" $ \stmt -> do+            before <- skipped+            replicateM_ 3 (queryPrepared stmt [] >>= S.skipToEof . snd)+            after <- skipped+            equal "server actually omitted all three metadata blocks" 3 (after-before)+    , case3 "cache.metadata-owns-buffer" $ do+        let packets = preparePackets 7+        sourceColumn <- decodeFromPacket (packets !! 1)+        (conn, _, _) <- cachedFixture packets+        stmt <- prepareStatement conn "SELECT 7"+        retained <- preparedMetadata stmt+        case retained of+            [metadata] -> do+                let (original, _, _) = BI.toForeignPtr (columnName sourceColumn)+                    (owned, _, _) = BI.toForeignPtr (columnName (metadataColumn metadata))+                assert (original /= owned) "cached metadata pins its receive buffer"+            _ -> fail "expected one retained column"+    , case3 "prepared.bracket-drains-on-success" $ withConn $ \conn -> do+        result <- withPreparedStatement conn "SELECT seq FROM seq_1_to_10" $ \stmt -> do+            (_, stream) <- queryPrepared stmt []+            void (S.read stream)+            pure (7 :: Int)+        equal "callback result preserved" 7 result+        healthy conn+        equal "statement released after drain" 0 . length =<< C.statementMap conn+    , case3 "prepared.cleanup-new-cancellation" $ do+        packets <- newIORef (preparePackets 7 ++ [putToPacket 1 (putWord8 1 >> putWord8 0)])+        input <- S.makeInputStream $ do+            next <- atomicModifyIORef' packets (\xs -> case xs of [] -> ([],Nothing); x:rest -> (rest,Just x))+            maybe (throwIO ThreadKilled) (pure . Just) next+        conn <- C.newConnection defaultConnectionOptions input (const (pure ())) (pure ())+        writeIORef (C.connectionCapabilities conn) (bit 24 .|. bit 36)+        result <- try $ withPreparedStatement conn "SELECT 7" $ \stmt -> do+            void (queryPrepared stmt [])+            throwIO (userError "earlier-primary")+        case result of+            Left ThreadKilled -> pure ()+            _ -> fail "new cleanup cancellation was replaced"+        equal "cancelled cleanup retires" Broken =<< connectionStatus conn+    , case3 "metadata.attribute-count-bound" $ do+        let packet = putToPacket 0 $ do+                mapM_ putLenEncBytes ["def","","","","n",""]+                putLenEncBytes (B.replicate 131072 0)+                putWord8 12; putWord16le 63; putWord32le 20+                putWord8 8; putWord16le 0; putWord8 0; putWord16le 0+        expectRejection (getFromPacketExact (getColumnMetadata True) packet)+    , case3 "bulk.minimum-size-before-cell-traversal" $ do+        (conn, sent, _) <- scripted defaultConnectionOptions {maximumMessageBytes=64} []+        writeIORef (C.connectionCapabilities conn) (bit 34)+        let invalidRows = replicate 1000 (error "oversized batch cells must not be visited")+            commandValue = COM_STMT_BULK_EXECUTE 7 128 [MySQLInt32 1] invalidRows+        result <- try (C.withCommand conn (C.sendCommand conn commandValue))+        case result of+            Left (ProtocolError _) -> pure ()+            _ -> fail "minimum encoded size was not rejected locally"+        equal "impossible batch sends nothing" [] =<< sent+        equal "early rejection preserves connection" Ready =<< connectionStatus conn+    ]+  where+    isOKResult (PreparedOK _) = True+    isOKResult _ = False
+ validation/hs/Validation/Stage3Bench.hs view
@@ -0,0 +1,82 @@+-- | Additive native-feature benchmarks. Frozen workloads remain unchanged.+-- Each profile performs two complete warm-ups before the measured region and+-- consumes/checks all values. Connections and statement preparation are outside+-- timing; batch transaction/truncate/verification work is identical in each mode.+module Validation.Stage3Bench (run) where++import Control.Exception (evaluate)+import Control.Monad+import Data.List (intercalate, isPrefixOf, isSuffixOf)+import qualified Data.Text as T+import Database.MySQL.Base+import GHC.Clock+import GHC.Stats+import System.Mem+import Validation.Common+import qualified System.IO.Streams as S++run :: String -> Int -> IO ()+run ident iterations+    | iterations <= 0 = fail "positive native benchmark count required"+    | "bulk." `isPrefixOf` ident = withConn $ \conn -> do+        exec conn "CREATE TEMPORARY TABLE mb_stage3_bench(n BIGINT NOT NULL,v VARCHAR(128) NOT NULL) ENGINE=InnoDB"+        withPreparedStatement conn "INSERT INTO mb_stage3_bench VALUES(?,?)" $ \stmt -> do+            let values = [[MySQLInt64 n,MySQLText (T.replicate 128 "x")] | n <- [1..1000]]+                batch = map (map BulkValue) values+                operation = do+                    exec conn "TRUNCATE mb_stage3_bench"+                    withTransaction conn $ case ident of+                        "bulk.native" -> do+                            ok <- executeBulk stmt batch+                            equal "bulk count" 1000 (okAffectedRows ok)+                        "bulk.units" -> do+                            result <- executeBulkUnitResults stmt batch+                            equal "unit count" 1000 (sum (map bulkAffectedRows result))+                        "bulk.prepared" -> forM_ values $ \row -> do+                            ok <- executePrepared stmt row+                            equal "prepared count" 1 (okAffectedRows ok)+                        "bulk.text-batch" -> do+                            result <- executeMany conn "INSERT INTO mb_stage3_bench VALUES(?,?)" values+                            equal "text-batch count" 1000 (sum (map okAffectedRows result))+                        _ -> fail "unknown bulk benchmark profile"+                    actual <- rows conn "SELECT COUNT(*),SUM(n),SUM(OCTET_LENGTH(v)) FROM mb_stage3_bench"+                    equal "stored rows and every payload" [[1000,500500,128000]] (map (map number) actual)+                    pure 629500+            measure ident (toInteger iterations * 1000) (toInteger iterations * 629500)+                (sum <$> replicateM iterations operation)+    | "prepared." `isPrefixOf` ident = do+        ci <- config+        let cached = ".cached" `isSuffixOf` ident+            wide = "prepared.wide." `isPrefixOf` ident+            queryText = if wide then sql ("SELECT 7 AS n," ++ intercalate ","+                ["REPEAT('x',128) AS v" ++ show n | n <- [1..30 :: Int]]) else "SELECT 7 AS n"+            expected = if wide then 3847 else 7+        withConnect (connectWithOptions defaultConnectionOptions {enableMetadataCaching=cached} ci) $ \conn ->+            withPreparedStatement conn queryText $ \stmt -> do+                let operation = do+                        (_, stream) <- queryPrepared stmt []+                        checksum <- S.fold (\total row -> total + sum (map value row)) 0 stream+                        equal "all prepared fields consumed" expected checksum+                        pure checksum+                measure ident (toInteger iterations) (toInteger iterations * expected)+                    (sum <$> replicateM iterations operation)+    | otherwise = fail "unknown native benchmark profile"+  where+    value (MySQLText text) = toInteger (T.length text)+    value other = number other++measure :: String -> Integer -> Integer -> IO Integer -> IO ()+measure ident units expected operation = do+    void operation+    void operation+    performGC+    before <- getRTSStats+    start <- getMonotonicTimeNSec+    checksum <- operation >>= evaluate+    end <- getMonotonicTimeNSec+    performGC+    after <- getRTSStats+    equal "measured checksum" expected checksum+    putStrLn (intercalate "\t" ["STAGE3",ident,show units,show (end-start),+        show (allocated_bytes after-allocated_bytes before),+        show (cpu_ns after-cpu_ns before),show (gc_cpu_ns after-gc_cpu_ns before),show checksum])
+ validation/hs/Validation/Stage4.hs view
@@ -0,0 +1,651 @@+{-# LANGUAGE ScopedTypeVariables #-}+-- | Session isolation checks use both literal wire envelopes and a real server.+-- Tests count actual writes and cover cancellation at restoration boundaries;+-- a successful RESET acknowledgement alone does not prove a reusable session.+module Validation.Stage4 (cases, resetFeature, trackingFeature) where++import Control.Concurrent+import Control.Exception hiding (assert)+import Control.Monad+import Data.Binary.Put+import Data.Bits+import qualified Data.ByteString as B+import qualified Data.ByteString.Internal as BI+import qualified Data.ByteString.Lazy as L+import Data.IORef+import Data.Word (Word8)+import qualified Data.Text.Encoding as TE+import qualified Data.Map.Strict as M+import Database.MySQL.Base+import qualified Database.MySQL.Connection as C+import qualified System.IO.Streams as S+import System.Timeout+import Validation.Common+import Validation.FaultTransport++case4 :: String -> IO () -> Case+case4 name = Case ("stage4." ++ name) "stage4"++-- Independent literal protocol construction: the parser is not used to encode+-- these fixtures, and malformed known types retain their proper outer length.+item :: Word8 -> Put -> Put+item kind body = putWord8 kind >> putLenEncBytes (L.toStrict (runPut body))++variable :: B.ByteString -> B.ByteString -> Put+variable name value = item 0 (putLenEncBytes name >> putLenEncBytes value)++tracked :: Put -> Packet+tracked changes = putToPacket 1 $ do+    putByteString (B.pack [0,0,0,2,64,0,0])+    putLenEncBytes ""+    putLenEncBytes (L.toStrict (runPut changes))++trackingFixture :: Put -> IO (MySQLConn, IO [Packet], IO Int)+trackingFixture changes = do+    result@(conn, _, _) <- scripted defaultConnectionOptions [tracked changes]+    writeIORef (C.connectionCapabilities conn) (bit 23 .|. bit 24)+    pure result++snapshotPackets :: [Packet]+snapshotPackets = roleSnapshotPackets (putWord8 0xfb)++-- Role is nullable, unlike the six mandatory rendering facts. Keep the literal+-- NULL marker independent of the implementation's row/role decoder.+roleSnapshotPackets :: Put -> [Packet]+roleSnapshotPackets role = [putToPacket 1 (putLenEncInt 7)] +++    [encodeToPacket n (ColumnDef "" "" "" "fact" "" 33 100 mySQLTypeVarString 0 0) | n <- [2..8]] +++    [encodeToPacket 9 (EOF 0 2), putToPacket 10 (mapM_ putLenEncBytes+        ["","utf8mb4","utf8mb4","utf8mb4_unicode_ci","12.3.3-MariaDB","67108864"] >> role),+     encodeToPacket 11 (EOF 0 2)]++resetFixture :: [Packet] -> IO (MySQLConn, IO [Packet], IO Int)+resetFixture packets = do+    result@(conn, _, _) <- scripted defaultConnectionOptions packets+    writeIORef (C.connectionInitialSession conn)+        (Just (C.InitialSession "db" "utf8mb4" "utf8mb4_unicode_ci" 120303 Nothing))+    writeIORef (C.connectionServerStatus conn) (Just 2)+    C.recordSessionChanges conn [SchemaChanged "db"]+    pure result++resetTranscript :: [Packet]+resetTranscript = encodeToPacket 1 (OK 0 0 2 0) : snapshotPackets++-- Stall after an optional acknowledged prefix, so deadlines/cancellation are+-- armed by an actual read boundary rather than a scheduler-dependent sleep.+stalledReset :: [Packet] -> IO (MySQLConn, IO (), IO Int)+stalledReset prefix = do+    packets <- newIORef prefix+    entered <- newEmptyMVar+    blocked <- newEmptyMVar+    released <- newIORef 0+    input <- S.makeInputStream $ do+        next <- atomicModifyIORef' packets (\remaining -> case remaining of+            [] -> ([],Nothing)+            first:rest -> (rest,Just first))+        case next of+            Just packet -> pure (Just packet)+            Nothing -> void (tryPutMVar entered ()) >> takeMVar blocked+    conn <- C.newConnection defaultConnectionOptions {cleanupTimeoutMicros=20000}+        input (const (pure ())) (modifyIORef' released (+1))+    writeIORef (C.connectionInitialSession conn)+        (Just (C.InitialSession "db" "utf8mb4" "utf8mb4_unicode_ci" 120303 Nothing))+    C.recordSessionFacts conn [SchemaChanged "db"]+    pure (conn, takeMVar entered, readIORef released)++resetFeature :: IO ()+resetFeature = withConn $ \conn -> do+    before <- scalarInt conn "SELECT CONNECTION_ID()"+    exec conn "SET @mb_stage4_user=7"+    exec conn "CREATE TEMPORARY TABLE mb_stage4_temp(n INT)"+    stmt <- prepareStatement conn "SELECT 7"+    void (resetConnection conn)+    equal "reset does not reconnect" before =<< scalarInt conn "SELECT CONNECTION_ID()"+    equal "user variables cleared" MySQLNull =<< scalar conn "SELECT @mb_stage4_user"+    expectERR [1146] (rows conn "SELECT * FROM mb_stage4_temp")+    expectRejection (preparedMetadata stmt)+    refreshSessionState conn+    healthy conn++trackingFeature :: IO ()+trackingFeature = withConn $ \conn -> do+    exec conn "SET time_zone='+01:00'"+    state <- getSessionState conn+    equal "tracked timezone exposed" (Just "+01:00") (M.lookup "time_zone" (sessionVariables state))+    equal "autocommit flag exposed" (Just True) (sessionAutocommit state)+    exec conn "START TRANSACTION"+    active <- getSessionState conn+    equal "transaction flag exposed" (Just True) (sessionInTransaction active)+    exec conn "ROLLBACK"+    equal "rollback flag exposed" (Just False) . sessionInTransaction =<< getSessionState conn++cases :: [Case]+cases =+    [ case4 "tracking.initial-state" $ withConn $ \conn -> do+        ci <- config+        state <- getSessionState conn+        equal "constructor schema observed" (Just (ciDatabase ci)) (sessionSchema state)+        equal "initial autocommit" (Just True) (sessionAutocommit state)+        equal "initial transaction" (Just False) (sessionInTransaction state)+        equal "rendering charset observed" (Just "utf8mb4")+            (M.lookup "character_set_client" (sessionVariables state))+    , case4 "tracking.all-kinds-and-wire-order" $ do+        let changes = do+                variable "time_zone" "+02:00"+                item 1 (putLenEncBytes "second")+                item 2 (putByteString "1")+                item 4 (putLenEncBytes "START TRANSACTION READ ONLY;")+                item 5 (putLenEncBytes "T_______")+                item 77 (putByteString "opaque")+        (conn, _, _) <- trackingFixture changes+        void (ping conn)+        state <- getSessionState conn+        equal "schema decoded" (Just "second") (sessionSchema state)+        equal "characteristics decoded" (Just "START TRANSACTION READ ONLY;") (sessionTransactionCharacteristics state)+        equal "transaction state decoded" (Just "T_______") (sessionTransactionState state)+        equal "all changes in wire order"+            [SystemVariableChanged "time_zone" "+02:00", SchemaChanged "second", SessionStateChanged True,+             TransactionCharacteristicsChanged "START TRANSACTION READ ONLY;", TransactionStateChanged "T_______",+             UnknownSessionChange 77 "opaque"] (sessionLastChanges state)+    , case4 "tracking.repeated-variable-last-wins" $ do+        (conn, _, _) <- trackingFixture (variable "time_zone" "+01:00" >> variable "time_zone" "+02:00")+        void (ping conn)+        equal "later variable replaces earlier" (Just "+02:00") . M.lookup "time_zone" . sessionVariables =<< getSessionState conn+    , case4 "tracking.snapshot-does-no-io" $ do+        (conn, sent, _) <- trackingFixture (variable "time_zone" "+01:00")+        void (ping conn)+        before <- sent+        replicateM_ 100 (void (getSessionState conn))+        equal "snapshot sends no SQL" before =<< sent+    , case4 "tracking.owns-receive-bytes" $ do+        (conn, _, _) <- trackingFixture (variable "time_zone" "+01:00")+        void (ping conn)+        state <- getSessionState conn+        case M.lookup "time_zone" (sessionVariables state) of+            Just value -> do+                let (_, offset, size) = BI.toForeignPtr value+                equal "retained value has exact owned storage" (0,6) (offset,size)+            Nothing -> fail "tracked value missing"+    , case4 "tracking.malformed-known-kinds" $ forM_+        [item 1 (putByteString "bad"), item 2 (putByteString "yes"),+         item 4 (putLenEncBytes "x" >> putWord8 0), item 5 (putLenEncBytes "short"),+         item 0 (putLenEncBytes "name"), variable "" "value"] $ \changes -> do+            (conn, _, released) <- trackingFixture changes+            expectRejection (ping conn)+            equal "malformed tracker retires" Broken =<< connectionStatus conn+            equal "malformed tracker releases once" 1 =<< released+    , case4 "tracking.item-count-bound" $ do+        (conn, _, _) <- trackingFixture (replicateM_ 65536 (item 77 (pure ())))+        expectRejection (ping conn)+        equal "empty items still consume the count budget" Broken =<< connectionStatus conn+    , case4 "tracking.length-overflow-rejected" $ do+        (conn, _, _) <- trackingFixture (putWord8 77 >> putWord8 254 >> putWord64le maxBound)+        expectRejection (ping conn)+        equal "encoded length never wraps to a small payload" Broken =<< connectionStatus conn+    , case4 "tracking.accumulated-budget" $ do+        (conn, _, _) <- scripted defaultConnectionOptions {maximumMetadataBytes=100} []+        C.recordSessionChanges conn [SystemVariableChanged "one" (B.replicate 30 65)]+        expectRejection (C.recordSessionChanges conn [SystemVariableChanged "two" (B.replicate 40 65)])+        equal "accumulated state cannot grow unbounded" Broken =<< connectionStatus conn+    , case4 "tracking.replacement-releases-budget" $ do+        (conn, _, _) <- scripted defaultConnectionOptions {maximumMetadataBytes=100} []+        replicateM_ 1000 (C.recordSessionChanges conn [SystemVariableChanged "same" (B.replicate 30 65)])+        equal "one retained variable" 1 . M.size . sessionVariables =<< getSessionState conn+    , case4 "tracking.explicit-facts-retained-envelope-budget" $ do+        (conn, _, _) <- scripted defaultConnectionOptions {maximumMetadataBytes=100} []+        C.recordSessionChanges conn [UnknownSessionChange 77 (B.replicate 80 65)]+        expectRejection (C.recordSessionFacts conn [SystemVariableChanged "name" (B.replicate 30 65)])+        equal "snapshot seeding charges the retained old envelope" Broken =<< connectionStatus conn+    , case4 "tracking.explicit-facts-do-not-forge-events" $ do+        (conn, _, _) <- trackingFixture (variable "time_zone" "+01:00")+        void (ping conn)+        before <- getSessionState conn+        C.recordSessionFacts conn [SchemaChanged "explicit"]+        after <- getSessionState conn+        equal "explicitly known schema" (Just "explicit") (sessionSchema after)+        equal "wire envelope preserved" (sessionLastChanges before) (sessionLastChanges after)+        equal "wire envelope counter preserved" (sessionChangeNumber before) (sessionChangeNumber after)+    , case4 "tracking.subscription-invalidates-values" $ do+        (conn, _, _) <- trackingFixture $ do+            variable "old" "stale"+            variable "time_zone" "+01:00"+            variable "session_track_system_variables" "time_zone"+        void (ping conn)+        state <- getSessionState conn+        equal "unsubscribed value discarded" Nothing (M.lookup "old" (sessionVariables state))+        equal "selected value preserved" (Just "+01:00") (M.lookup "time_zone" (sessionVariables state))+    , case4 "tracking.schema-and-transaction-controls" $ do+        (conn, _, _) <- trackingFixture $ do+            item 1 (putLenEncBytes "old")+            item 4 (putLenEncBytes "START TRANSACTION;")+            item 5 (putLenEncBytes "T_______")+            variable "session_track_schema" "OFF"+            variable "session_track_transaction_info" "OFF"+        void (ping conn)+        state <- getSessionState conn+        equal "untracked schema becomes unknown" Nothing (sessionSchema state)+        equal "untracked transaction details become unknown" Nothing (sessionTransactionState state)+        equal "untracked transaction characteristics become unknown" Nothing (sessionTransactionCharacteristics state)+    , case4 "tracking.collation-direct-live" $ withConn $ \conn -> do+        exec conn "SET collation_connection='utf8mb4_bin'"+        state <- getSessionState conn+        equal "unreported collation is explicitly unknown" Nothing+            (M.lookup "collation_connection" (sessionVariables state))+    , case4 "tracking.collation-set-names-live" $ withConn $ \conn -> do+        exec conn "SET NAMES latin1"+        state <- getSessionState conn+        equal "connection charset changed" (Just "latin1")+            (M.lookup "character_set_connection" (sessionVariables state))+        equal "SET NAMES leaves unreported collation explicitly unknown" Nothing+            (M.lookup "collation_connection" (sessionVariables state))+    , case4 "tracking.collation-delta-order" $ forM_ [False, True] $ \collationFirst -> do+        let charset = variable "character_set_connection" "latin1"+            collation = variable "collation_connection" "latin1_bin"+        (conn, _, _) <- trackingFixture (if collationFirst then collation >> charset else charset >> collation)+        void (ping conn)+        equal "explicit collation survives either wire order" (Just "latin1_bin")+            . M.lookup "collation_connection" . sessionVariables =<< getSessionState conn+    , case4 "tracking.collation-invalidation-releases-budget" $ do+        (conn, _, _) <- scripted defaultConnectionOptions {maximumMetadataBytes=70} []+        C.recordSessionFacts conn [SystemVariableChanged "collation_connection" (B.replicate 30 65)]+        C.recordSessionChanges conn [SystemVariableChanged "character_set_connection" "latin1"]+        state <- getSessionState conn+        equal "unreported dependent collation discarded" Nothing (M.lookup "collation_connection" (sessionVariables state))+        equal "discarded collation releases its byte charge" Ready =<< connectionStatus conn+    , case4 "tracking.collation-explicit-selection" $ withConn $ \conn -> do+        -- Collation first selects that alias for their shared server offset.+        exec conn "SET session_track_system_variables='sql_mode,character_set_client,collation_connection'"+        refreshSessionState conn+        exec conn "SET collation_connection='latin1_bin'"+        state <- getSessionState conn+        equal "explicitly subscribed collation observed" (Just "latin1_bin")+            (M.lookup "collation_connection" (sessionVariables state))+        equal "unreported charset cannot remain stale" Nothing+            (M.lookup "character_set_connection" (sessionVariables state))+        expectRejection (getRenderContext conn)+        refreshSessionState conn+        equal "explicit refresh establishes the new charset" "latin1"+            . renderConnectionCharset =<< getRenderContext conn+    , case4 "tracking.transaction-characteristics-downgrade" $ do+        (conn, _, _) <- trackingFixture $ do+            item 4 (putLenEncBytes "START TRANSACTION READ ONLY;")+            item 5 (putLenEncBytes "T_______")+            variable "session_track_transaction_info" "STATE"+        void (ping conn)+        state <- getSessionState conn+        equal "STATE retains transaction state" (Just "T_______") (sessionTransactionState state)+        equal "STATE cannot retain characteristics" Nothing (sessionTransactionCharacteristics state)+    , case4 "tracking.transaction-characteristics-downgrade-live" $ withConn $ \conn -> do+        exec conn "SET session_track_transaction_info='CHARACTERISTICS'"+        exec conn "START TRANSACTION READ ONLY"+        before <- getSessionState conn+        equal "read-only characteristics observed" (Just "START TRANSACTION READ ONLY;")+            (sessionTransactionCharacteristics before)+        exec conn "SET session_track_transaction_info='STATE'"+        exec conn "ROLLBACK"+        exec conn "START TRANSACTION READ WRITE"+        after <- getSessionState conn+        equal "new transaction state remains observed" (Just "T_______") (sessionTransactionState after)+        equal "previous transaction's replay text discarded" Nothing (sessionTransactionCharacteristics after)+        exec conn "ROLLBACK"+    , case4 "tracking.live-schema" $ withConn $ \conn -> do+        void (command conn (COM_INIT_DB "information_schema"))+        equal "COM_INIT_DB schema reported" (Just "information_schema") . sessionSchema =<< getSessionState conn+        ci <- config+        exec conn ("USE `" ++ map (toEnum . fromIntegral) (B.unpack (ciDatabase ci)) ++ "`")+        equal "USE schema reported" (Just (ciDatabase ci)) . sessionSchema =<< getSessionState conn+    , case4 "tracking.live-transaction-details" $ withConn $ \conn -> do+        exec conn "SET session_track_transaction_info='CHARACTERISTICS'"+        exec conn "START TRANSACTION READ ONLY"+        state <- getSessionState conn+        assert (sessionTransactionState state /= Nothing) "transaction state not exposed"+        assert (sessionTransactionCharacteristics state /= Nothing) "transaction characteristics not exposed"+        exec conn "ROLLBACK"+    , case4 "tracking.multiple-results-retain-real-envelope" $ withConn $ \conn -> do+        void (queryMulti_ conn "SET time_zone='+01:00'; SET time_zone='+02:00'; SELECT 7")+        state <- getSessionState conn+        assert (SystemVariableChanged "time_zone" "+02:00" `elem` sessionLastChanges state)+            "opaque multi-statement processing lost the latest actual tracker envelope"+        equal "opaque final facts remain conservatively unknown" M.empty (sessionVariables state)+    , case4 "tracking.disabled-transaction-flags" $ do+        ci <- config+        withConnect (connectWithOptions defaultConnectionOptions {enableSessionTracking=False} ci) $ \conn -> do+            exec conn "START TRANSACTION"+            equal "legacy status tracks transaction" (Just True) . sessionInTransaction =<< getSessionState conn+            exec conn "ROLLBACK"+            equal "legacy status tracks rollback" (Just False) . sessionInTransaction =<< getSessionState conn+    , case4 "tracking.error-invalidates" $ withConn $ \conn -> do+        expectERR [1064] (exec conn "SELECT FROM")+        state <- getSessionState conn+        equal "ERR invalidates schema" Nothing (sessionSchema state)+        equal "ERR invalidates variable observations" M.empty (sessionVariables state)+        equal "ERR invalidates transaction flags" Nothing (sessionInTransaction state)+        healthy conn+        equal "next complete reply restores transaction flag" (Just False) . sessionInTransaction =<< getSessionState conn+    , case4 "tracking.disabled-honest-unknowns" $ do+        ci <- config+        withConnect (connectWithOptions defaultConnectionOptions {enableSessionTracking=False} ci) $ \conn -> do+            exec conn "SET time_zone='+01:00'"+            state <- getSessionState conn+            equal "untracked variables unknown" M.empty (sessionVariables state)+            equal "wire transaction flags remain known" (Just False) (sessionInTransaction state)+    , case4 "reset.same-session-and-cleanup" resetFeature+    , case4 "reset.rolls-back-open-transaction" $ withConn $ \conn -> do+        exec conn "DROP TABLE IF EXISTS mb_stage4_rollback"+        exec conn "CREATE TABLE mb_stage4_rollback(n INT) ENGINE=InnoDB"+        flip finally (exec conn "DROP TABLE IF EXISTS mb_stage4_rollback") $ do+            exec conn "START TRANSACTION"+            exec conn "INSERT INTO mb_stage4_rollback VALUES(7)"+            void (resetConnection conn)+            equal "uncommitted insert rolled back" 0 =<< scalarInt conn "SELECT COUNT(*) FROM mb_stage4_rollback"+    , case4 "reset.restores-constructor-database" $ withConn $ \conn -> do+        ci <- config+        exec conn "USE information_schema"+        void (resetConnection conn)+        equal "constructor schema selected" (MySQLText (textOf (MySQLBytes (ciDatabase ci)))) =<< scalar conn "SELECT DATABASE()"+        equal "public schema matches" (Just (ciDatabase ci)) . sessionSchema =<< getSessionState conn+    , case4 "reset.restores-charset-and-rendering" $ withConn $ \conn -> do+        exec conn "SET NAMES latin1"+        exec conn "SET sql_mode='NO_BACKSLASH_ESCAPES'"+        void (resetConnection conn)+        equal "original client charset" "utf8mb4" . renderClientCharset =<< getRenderContext conn+        (_, stream) <- query conn "SELECT ?" [MySQLText "unicode \x1f642 '"]+        equal "rendering after reset" [[MySQLText "unicode \x1f642 '"]] =<< S.toList stream+    , case4 "reset.custom-initializer" $ withConn $ \conn -> do+        void $ resetConnectionWith conn $ \session -> exec session+            "SET time_zone='+00:00',sql_mode='ANSI_QUOTES,STRICT_TRANS_TABLES',autocommit=1"+        equal "required settings restored" [[MySQLText "+00:00",MySQLText "ANSI_QUOTES,STRICT_TRANS_TABLES"]]+            =<< rows conn "SELECT @@time_zone,@@sql_mode"+        equal "custom modes reflected" ["ANSI_QUOTES","STRICT_TRANS_TABLES"] . renderSQLModes =<< getRenderContext conn+    , case4 "reset.role-none-before-initializer" $ do+        ci <- config+        withConnect (connect ci {ciUser="mariadb_role_none"}) $ \conn -> do+            equal "fixture starts without role" MySQLNull =<< scalar conn "SELECT CURRENT_ROLE()"+            refreshSessionState conn+            void (execute conn "SET ROLE ?" [MySQLText "mb_stage4_rôle_'`"])+            void $ resetConnectionWith conn $ \session ->+                equal "role cleared before application initialization" MySQLNull+                    =<< scalar session "SELECT CURRENT_ROLE()"+    , case4 "reset.role-default-before-database" $ do+        ci <- config+        withConnect (connect ci {ciUser="mariadb_role_default"}) $ \conn -> do+            equal "quoted Unicode default role active" (MySQLText "mb_stage4_rôle_'`")+                =<< scalar conn "SELECT CURRENT_ROLE()"+            exec conn "SET ROLE mb_stage4_other_role"+            exec conn "USE information_schema"+            -- Only the initial role grants access to the constructor database.+            -- Reversing restoration order fails with an actual access error.+            void (resetConnection conn)+            equal "constructor role restored" (MySQLText "mb_stage4_rôle_'`")+                =<< scalar conn "SELECT CURRENT_ROLE()"+            equal "constructor database accessible after role restoration" (MySQLText "mariadb_haskell_validation")+                =<< scalar conn "SELECT DATABASE()"+    , case4 "reset.role-initializer-and-refresh" $ do+        ci <- config+        withConnect (connect ci {ciUser="mariadb_role_none"}) $ \conn -> do+            void $ resetConnectionWith conn $ \session ->+                exec session "SET ROLE mb_stage4_other_role"+            equal "initializer may choose a request role" (MySQLText "mb_stage4_other_role")+                =<< scalar conn "SELECT CURRENT_ROLE()"+            refreshSessionState conn+            void (resetConnection conn)+            equal "refresh never redefines constructor role" MySQLNull+                =<< scalar conn "SELECT CURRENT_ROLE()"+    , case4 "reset.role-without-tracking" $ do+        ci <- config+        withConnect (connectWithOptions defaultConnectionOptions {enableSessionTracking=False}+            ci {ciUser="mariadb_role_default"}) $ \conn -> do+            exec conn "SET ROLE mb_stage4_other_role"+            exec conn "USE information_schema"+            void (resetConnection conn)+            equal "role restored without tracker assistance" (MySQLText "mb_stage4_rôle_'`")+                =<< scalar conn "SELECT CURRENT_ROLE()"+    , case4 "reset.role-latin1-client" $ do+        ci <- config+        withConnect (connect ci {ciUser="mariadb_role_default",ciCharset=8}) $ \conn -> do+            exec conn "SET ROLE mb_stage4_other_role"+            void (resetConnection conn)+            equal "role literal respects the client encoding" (MySQLText "mb_stage4_rôle_'`")+                =<< scalar conn "SELECT CURRENT_ROLE()"+    , case4 "reset.role-lossy-result-charset" $ do+        ci <- config+        withConnect (connect ci {ciUser="mariadb_role_wide",ciCharset=8}) $ \conn -> do+            initial <- readIORef (C.connectionInitialSession conn)+            equal "constructor retains original UTF-8 role bytes" (Just (TE.encodeUtf8 "mb_stage4_汉"))+                (C.initialRole =<< initial)+            -- Both names become mb_stage4_? in latin1 text results. Only the+            -- binary snapshot can distinguish them without lossy conversion.+            exec conn "SET NAMES utf8mb4"+            exec conn "SET ROLE `mb_stage4_字`"+            exec conn "SET NAMES latin1"+            void (resetConnection conn)+            equal "narrow client charset restored after the Unicode role" "latin1"+                . renderClientCharset =<< getRenderContext conn+            equal "different Unicode role cannot survive a lossy comparison"+                (MySQLBytes (TE.encodeUtf8 "mb_stage4_汉"))+                =<< scalar conn "SELECT CAST(CURRENT_ROLE() AS BINARY)"+    , case4 "reset.role-binary-result-charset" $ do+        ci <- config+        withConnect (connect ci {ciUser="mariadb_role_wide"}) $ \conn -> do+            exec conn "SET character_set_results=binary"+            exec conn "SET ROLE `mb_stage4_字`"+            void (resetConnection conn)+            equal "binary results preserve role identity" (MySQLBytes (TE.encodeUtf8 "mb_stage4_汉"))+                =<< scalar conn "SELECT CAST(CURRENT_ROLE() AS BINARY)"+    , case4 "reset.role-tls-and-socket" $ do+        ci <- config+        path <- socketPath+        name <- env "MB_TLS_NAME" "localhost"+        forM_ [tlsConnect ci {ciUser="mariadb_role_none"} name,+               connectUnixSocket path ci {ciUser="mariadb_role_none"}] $ \open ->+            withConnect open $ \conn -> do+                exec conn "SET ROLE mb_stage4_other_role"+                void (resetConnection conn)+                equal "transport reset clears prior role" MySQLNull =<< scalar conn "SELECT CURRENT_ROLE()"+    , case4 "reset.tls" $ withTLS $ \conn -> void (resetConnection conn) >> healthy conn+    , case4 "reset.socket" $ do+        ci <- config+        path <- socketPath+        withConnect (connectUnixSocket path ci) $ \conn -> void (resetConnection conn) >> healthy conn+    , case4 "reset.tracking-disabled" $ do+        ci <- config+        withConnect (connectWithOptions defaultConnectionOptions {enableSessionTracking=False} ci) $ \conn -> do+            void (resetConnection conn)+            (_, stream) <- query conn "SELECT ?" [MySQLText "safe '"]+            equal "explicit reset snapshot works without tracking" [[MySQLText "safe '"]] =<< S.toList stream+    , case4 "reset.raw-command-uses-guarded-restoration" $ withConn $ \conn -> do+        exec conn "USE information_schema"+        void (command conn COM_RESET_CONNECTION)+        ci <- config+        equal "low-level public command restores constructor schema" (Just (ciDatabase ci)) . sessionSchema =<< getSessionState conn+    , case4 "reset.no-database-unchanged-is-safe" $ do+        ci <- config+        withConnect (connect ci {ciDatabase=""}) $ \conn -> do+            void (resetConnection conn)+            equal "no selected database preserved honestly" MySQLNull =<< scalar conn "SELECT DATABASE()"+    , case4 "reset.autocommit-and-isolation-defaults" $ withConn $ \conn -> do+        expected <- rows conn "SELECT @@autocommit,@@tx_isolation"+        exec conn "SET autocommit=0"+        exec conn "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED"+        void (resetConnection conn)+        equal "transaction settings return to defaults" expected =<< rows conn "SELECT @@autocommit,@@tx_isolation"+    , case4 "reset.after-transaction-tracking" $ withConn $ \conn -> do+        exec conn "SET session_track_transaction_info='CHARACTERISTICS',session_track_state_change=ON"+        exec conn "START TRANSACTION READ ONLY"+        void (resetConnection conn)+        state <- getSessionState conn+        equal "tracked transaction reset is synchronized" (Just False) (sessionInTransaction state)+        equal "old transaction characteristics discarded" Nothing (sessionTransactionCharacteristics state)+        equal "old transaction state discarded" Nothing (sessionTransactionState state)+        healthy conn+    , case4 "reset.releases-advisory-locks" $ withConn $ \conn -> do+        equal "test advisory lock acquired" 1 =<< scalarInt conn "SELECT GET_LOCK('mb_stage4_reset_lock',0)"+        void (resetConnection conn)+        equal "next borrower cannot inherit an advisory lock" 1+            =<< scalarInt conn "SELECT IS_FREE_LOCK('mb_stage4_reset_lock')"+    , case4 "reset.empty-database-after-use-rejected" $ do+        ci <- config+        withConnect (connect ci {ciDatabase=""}) $ \conn -> do+            void (command conn (COM_INIT_DB (ciDatabase ci)))+            expectRejection (resetConnection conn)+            equal "local restoration rejection leaves session intact" Ready =<< connectionStatus conn+    , case4 "reset.stale-handle-and-id-reuse" $ withConn $ \conn -> do+        old <- prepareStatement conn "SELECT 7"+        raw <- prepareStmt conn "SELECT 8"+        void (resetConnection conn)+        expectRejection (queryPrepared old [])+        expectERR [1243] (queryStmt conn raw [])+        refreshSessionState conn+        withPreparedStatement conn "SELECT 9" $ \fresh -> do+            expectRejection (preparedMetadata old)+            equal "new handle works" [[9]] . map (map number) =<< (queryPrepared fresh [] >>= S.toList . snd)+    , case4 "reset.registry-bounded-over-reuse" $ withConn $ \conn -> replicateM_ 100 $ do+        void (prepareStatement conn "SELECT REPEAT('x',1000)")+        void (resetConnection conn)+        registry <- readIORef (C.connectionStatements conn)+        equal "all cached metadata released" (Just 0) (C.registryMetadataBytes <$> registry)+        equal "no stale registrations retained" 0 . length =<< C.statementMap conn+    , case4 "reset.pending-stream-rejected-before-send" $ withConn $ \conn -> do+        (_, stream) <- query_ conn "SELECT seq FROM seq_1_to_10"+        outcome <- try (resetConnection conn)+        case outcome of Left UnconsumedResultSet -> pure (); _ -> fail "reset consumed pending rows"+        equal "pending rows intact" 10 . length =<< S.toList stream+        healthy conn+    , case4 "reset.transaction-helper-rejected" $ withConn $ \conn -> withTransaction conn $ do+        outcome <- try (resetConnection conn)+        case outcome of Left NestedTransactionUnsupported -> pure (); _ -> fail "reset escaped transaction lease"+        equal "outer transaction remains active" 1 =<< scalarInt conn "SELECT @@in_transaction"+    , case4 "reset.initializer-error-retires" $ withConn $ \conn -> do+        result <- try (resetConnectionWith conn (\_ -> fail "initializer-marker"))+        case result of Left (_ :: SomeException) -> pure (); _ -> fail "initializer error swallowed"+        equal "failed initialization cannot enter a pool" Broken =<< connectionStatus conn+    , case4 "reset.initializer-stream-retires" $ withConn $ \conn -> do+        expectRejection (resetConnectionWith conn (\session -> void (query_ session "SELECT seq FROM seq_1_to_10")))+        equal "unfinished initializer result retires" Broken =<< connectionStatus conn+    , case4 "reset.initializer-transaction-retires" $ withConn $ \conn -> do+        expectRejection (resetConnectionWith conn (\session -> exec session "START TRANSACTION"))+        equal "open initializer transaction retires" Broken =<< connectionStatus conn+    , case4 "reset.malformed-reply-retires" $ do+        (conn, _, released) <- resetFixture [putToPacket 1 (putWord8 1)]+        expectRejection (resetConnection conn)+        equal "malformed RESET reply retires" Broken =<< connectionStatus conn+        equal "transport released once" 1 =<< released+    , case4 "reset.server-error-retires" $ do+        (conn, _, released) <- resetFixture [encodeToPacket 1 (ERR 1047 "08S01" "reset unavailable")]+        expectERR [1047] (resetConnection conn)+        equal "RESET ERR retires conservatively" Broken =<< connectionStatus conn+        equal "RESET ERR releases once" 1 =<< released+    , case4 "reset.restore-error-retires" $ do+        (conn, _, released) <- resetFixture+            [encodeToPacket 1 (OK 0 0 2 0), encodeToPacket 1 (ERR 1064 "42000" "snapshot unavailable")]+        expectERR [1064] (resetConnection conn)+        equal "restoration error retires" Broken =<< connectionStatus conn+        equal "restoration releases once" 1 =<< released+    , case4 "reset.wire-byte-and-no-redundant-setup" $ do+        (conn, sent, _) <- resetFixture resetTranscript+        void (resetConnection conn)+        commands <- sent+        equal "RESET then one snapshot; legacy tracking has no setup SET" [31,3]+            (map (L.head . pBody) commands)+        case commands of+            first : _ -> equal "RESET is exactly one byte" (L.singleton 31) (pBody first)+            [] -> fail "RESET was not sent"+    , case4 "reset.role-restoration-error-retires" $ do+        (conn, sent, released) <- resetFixture (encodeToPacket 1 (OK 0 0 2 0)+            : roleSnapshotPackets (putLenEncBytes "different") ++ [encodeToPacket 1 (ERR 1959 "OP000" "role unavailable")])+        expectERR [1959] (resetConnection conn)+        equal "failed role restoration retires" Broken =<< connectionStatus conn+        equal "role failure releases once" 1 =<< released+        commands <- sent+        case reverse commands of+            role : _ -> equal "role command precedes initializer" "\x03SET ROLE NONE" (pBody role)+            [] -> fail "role restoration sent no command"+    , case4 "reset.role-snapshot-value-bound" $ do+        (conn, _, released) <- resetFixture (encodeToPacket 1 (OK 0 0 2 0)+            : roleSnapshotPackets (putLenEncBytes (B.replicate 4097 65)))+        expectRejection (resetConnection conn)+        equal "oversized role rejected under ownership" Broken =<< connectionStatus conn+        equal "oversized role releases once" 1 =<< released+    , case4 "reset.role-restoration-deadline" $ do+        (conn, _, released) <- stalledReset (encodeToPacket 1 (OK 0 0 2 0)+            : roleSnapshotPackets (putLenEncBytes "different"))+        result <- timeout 1000000 (try (resetConnection conn))+        case result of Just (Left CleanupDeadlineExceeded) -> pure (); _ -> fail "role restoration ignored cleanup deadline"+        equal "stalled role restoration retires" Broken =<< connectionStatus conn+        equal "role deadline releases once" 1 =<< released+    , case4 "reset.known-unsupported-version-no-send" $ do+        (conn, sent, _) <- resetFixture []+        writeIORef (C.connectionInitialSession conn) (Just (C.InitialSession "db" "utf8mb4" "utf8mb4_unicode_ci" 100203 Nothing))+        expectRejection (resetConnection conn)+        equal "version rejection sends nothing" [] =<< sent+        equal "version rejection preserves connection" Ready =<< connectionStatus conn+    , case4 "reset.no-database-unsafe-restore-rejected" $ do+        (conn, sent, _) <- resetFixture []+        writeIORef (C.connectionInitialSession conn) (Just (C.InitialSession "" "utf8mb4" "utf8mb4_unicode_ci" 120303 Nothing))+        expectRejection (resetConnection conn)+        equal "cannot silently preserve another borrower's database" [] =<< sent+    , case4 "reset.closed-and-broken-never-revived" $ do+        forM_ [close, \conn -> C.abortConnection conn False] $ \retire -> do+            (conn, sent, _) <- resetFixture []+            retire conn+            before <- sent+            expectRejection (resetConnection conn)+            equal "terminal reset sends nothing" before =<< sent+            equal "terminal snapshot unknown" Nothing . sessionSchema =<< getSessionState conn+    , case4 "reset.buffered-initializer-deadline" $ do+        (conn, _, released) <- resetFixture resetTranscript+        counter <- newIORef (0 :: Int)+        let bounded = conn {C.connectionOptions=defaultConnectionOptions {cleanupTimeoutMicros=20000}}+            spin = let loop = modifyIORef' counter (+1) >> loop in loop+        result <- timeout 1000000 (try (resetConnectionWith bounded (\_ -> spin)))+        case result of Just (Left CleanupDeadlineExceeded) -> pure (); _ -> fail "buffered initializer ignored deadline"+        equal "buffered timeout retires" Broken =<< connectionStatus bounded+        equal "deadline releases once" 1 =<< released+    , case4 "reset.wire-and-restoration-deadlines" $ forM_+        [[], [encodeToPacket 1 (OK 0 0 2 0)]] $ \prefix -> do+            (conn, _, released) <- stalledReset prefix+            result <- timeout 1000000 (try (resetConnection conn))+            case result of Just (Left CleanupDeadlineExceeded) -> pure (); _ -> fail "reset/restoration read ignored deadline"+            equal "stalled reset/restoration retires" Broken =<< connectionStatus conn+            equal "stalled read releases once" 1 =<< released+    , case4 "reset.wire-cancellation" $ do+        (conn, entered, released) <- stalledReset []+        let patient = conn {C.connectionOptions=defaultConnectionOptions {cleanupTimeoutMicros=5000000}}+        done <- newEmptyMVar+        worker <- forkFinally (resetConnection patient) (putMVar done)+        entered+        killThread worker+        result <- takeMVar done+        case result of Left err | Just ThreadKilled <- fromException err -> pure (); _ -> fail "wire cancellation lost identity"+        equal "wire cancellation retires" Broken =<< connectionStatus patient+        equal "wire cancellation releases once" 1 =<< released+    , case4 "reset.close-race-stays-terminal" $ do+        (conn, _, released) <- resetFixture resetTranscript+        entered <- newEmptyMVar+        proceed <- newEmptyMVar+        done <- newEmptyMVar+        worker <- forkFinally+            (resetConnectionWith conn (\_ -> putMVar entered () >> takeMVar proceed)) (putMVar done)+        flip finally (killThread worker) $ do+            takeMVar entered+            close conn+            putMVar proceed ()+            result <- takeMVar done+            case result of Left _ -> pure (); _ -> fail "closed reset was reported successful"+            equal "concurrent close stays terminal" Closed =<< connectionStatus conn+            equal "concurrent close does not revive registry" Nothing . fmap C.registryMetadataBytes+                =<< readIORef (C.connectionStatements conn)+            equal "close race releases once" 1 =<< released+    , case4 "reset.initializer-cancellation-and-exclusive-lease" $ do+        (conn, _, released) <- resetFixture resetTranscript+        entered <- newEmptyMVar+        wait <- newEmptyMVar+        done <- newEmptyMVar+        worker <- forkFinally (resetConnectionWith conn (\_ -> putMVar entered () >> takeMVar wait)) (putMVar done)+        takeMVar entered+        expectRejection (ping conn)+        killThread worker+        result <- takeMVar done+        case result of Left err | Just ThreadKilled <- fromException err -> pure (); _ -> fail "reset swallowed cancellation"+        equal "cancelled initializer retires" Broken =<< connectionStatus conn+        equal "cancellation releases once" 1 =<< released+    ]
+ validation/hs/Validation/Stage4Bench.hs view
@@ -0,0 +1,75 @@+-- | Complete request-boundary costs: reset/restore/prepare versus opening,+-- initializing, preparing and closing a new connection. Both paths perform the+-- same application work and deliberately leave state for the next boundary.+module Validation.Stage4Bench (run) where++import Control.Exception (bracket, evaluate)+import Control.Monad+import Data.List (intercalate, isSuffixOf, isInfixOf)+import Database.MySQL.Base+import GHC.Clock+import GHC.Stats+import System.Mem+import qualified System.IO.Streams as S+import Validation.Common++run :: String -> Int -> IO ()+run ident iterations+    | iterations <= 0 = fail "positive reuse benchmark count required"+    | ident == "session.snapshot" = withConn $ \conn -> do+        let loop 0 !total = pure (toInteger total)+            loop remaining !total = do+                state <- getSessionState conn+                equal "autocommit observed" (Just True) (sessionAutocommit state)+                equal "no transaction observed" (Just False) (sessionInTransaction state)+                loop (remaining - 1) (total + (1 :: Int))+        measure ident iterations (loop iterations 0)+    | otherwise = do+        ci <- config+        path <- socketPath+        name <- env "MB_TLS_NAME" "localhost"+        let open | ".tcp." `isInfixOf` ident = connect ci+                 | ".tls." `isInfixOf` ident = tlsConnect ci name+                 | ".socket." `isInfixOf` ident = connectUnixSocket path ci+                 | otherwise = fail "unknown reuse transport"+            initialize conn = exec conn "SET time_zone='+00:00',sql_mode='STRICT_TRANS_TABLES',autocommit=1"+            request conn = do+                -- Preparation is intentionally inside timing. RESET and a new+                -- connection both remove the preceding request's statement.+                statement <- prepareStatement conn "SELECT 7,@@time_zone,@@sql_mode,@mb_stage4_bench IS NULL"+                (_, stream) <- queryPrepared statement []+                result <- S.toList stream+                case result of+                    [[n,MySQLText zone,MySQLText modes,clean]] -> do+                        equal "request result" 7 (number n)+                        equal "application timezone restored" "+00:00" zone+                        equal "application SQL mode restored" "STRICT_TRANS_TABLES" modes+                        equal "no prior request's variable" 1 (number clean)+                    _ -> fail "reuse benchmark request shape mismatch"+                exec conn "SET @mb_stage4_bench=7,time_zone='+01:00'"+                pure 1+        if ".reset" `isSuffixOf` ident+        then withConnect open $ \conn -> measure ident iterations $ sum <$> replicateM iterations+            (resetConnectionWith conn initialize >> request conn)+        else if ".reconnect" `isSuffixOf` ident+        then measure ident iterations $ sum <$> replicateM iterations+            (bracket open close (\conn -> initialize conn >> request conn))+        else fail "unknown reuse benchmark mode"++-- Same warmups/GC/RTS convention as the native recorder. Units are complete+-- requests, or snapshot calls for session.snapshot; the checksum is one/unit.+measure :: String -> Int -> IO Integer -> IO ()+measure ident count operation = do+    void operation+    void operation+    performGC+    before <- getRTSStats+    start <- getMonotonicTimeNSec+    checksum <- operation >>= evaluate+    end <- getMonotonicTimeNSec+    performGC+    after <- getRTSStats+    equal "reuse benchmark checksum" (toInteger count) checksum+    putStrLn (intercalate "\t" ["STAGE4",ident,show count,show (end-start),+        show (allocated_bytes after-allocated_bytes before),show (cpu_ns after-cpu_ns before),+        show (gc_cpu_ns after-gc_cpu_ns before),show checksum])
+ validation/hs/Validation/Stage5.hs view
@@ -0,0 +1,592 @@+{-# LANGUAGE ScopedTypeVariables #-}+-- | Independent literal events plus real MariaDB transactions. Fixtures encode+-- bytes directly rather than serializing through the decoder under test.+module Validation.Stage5 (cases, gtidResumeFeature, manualAckFeature,+    fixtureFormat, tableMap, writeRow, packet, transaction, decode,+    withIsolated, sourceGtids, ackFixture) where++import Control.Concurrent+import Control.Exception hiding (assert)+import Control.Monad+import qualified Codec.Compression.Zlib as Z+import Data.Binary.Put+import Data.Bits+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as L+import Data.IORef+import Data.Maybe (mapMaybe)+import qualified Data.Text.Encoding as TE+import Data.Word+import Database.MySQL.Base+import Database.MySQL.BinLog+import qualified Database.MySQL.Connection as C+import qualified System.IO.Streams as S+import System.Environment (lookupEnv, getExecutablePath)+import System.Directory (doesFileExist)+import System.FilePath ((</>))+import System.Process (callProcess)+import System.Timeout+import Validation.Common+import Validation.FaultTransport (scripted, okPacket)++case5 :: String -> IO () -> Case+case5 name = Case ("stage5." ++ name) "stage5"++fixtureFormat :: FormatDescription+fixtureFormat = FormatDescription 4 "12.3.3-MariaDB" 0+    (B.pack [if kind `elem` [30,31,32,169,170,171] then 10 else if kind==172 then 9 else 8 | kind <- [1..172] :: [Int]])++packet :: BinLogEventType -> Put -> BinLogPacket+packet typ body = BinLogPacket 0 typ 610123 (fromIntegral (19 + L.length bytes)) 100 0 bytes False+  where bytes = runPut body++tableMap :: Word64 -> [Word8] -> BinLogPacket+tableMap tid types = packet BINLOG_TABLE_MAP_EVENT $ do+    putWord48le tid >> putWord16le 0+    putWord8 1 >> putByteString "d" >> putWord8 0+    putWord8 1 >> putByteString "t" >> putWord8 0+    putLenEncInt (length types) >> mapM_ putWord8 types+    putLenEncInt 0 >> putByteString (B.replicate ((length types + 7) `div` 8) 0)++writeRow :: Word64 -> Word16 -> Int -> BinLogPacket+writeRow tid flags value = packet BINLOG_WRITE_ROWS_EVENTv1 $ do+    putWord48le tid >> putWord16le flags >> putLenEncInt 1+    putWord8 1 >> putWord8 0 >> putWord32le (fromIntegral value)++gtid :: Word32 -> Word64 -> Word8 -> BinLogPacket+gtid domain sequenceNo flags = packet MARIADB_GTID_EVENT+    -- MariaDB's GTID_HEADER_LEN is 19: the 13 mandatory bytes are zero-padded.+    (putWord64le sequenceNo >> putWord32le domain >> putWord8 flags >> putByteString (B.replicate 6 0))++queryEvent :: B.ByteString -> BinLogPacket+queryEvent statement = packet BINLOG_QUERY_EVENT $ do+    putWord32le 0 >> putWord32le 0 >> putWord8 0 >> putWord16le 0 >> putWord16le 0 >> putWord8 0 >> putByteString statement++transaction :: Word64 -> Int -> [BinLogPacket]+transaction sequenceNo value = [gtid 0 sequenceNo 4, tableMap 1 [3], writeRow 1 1 value,+    packet BINLOG_XID_EVENT (putWord64le sequenceNo)]++emptyGtids :: GtidSet+emptyGtids = either error id (gtidSet [])++decode :: DecodeLimits -> [BinLogPacket] -> IO [CDCEvent]+decode limits events = do+    input <- S.fromList events+    (output, _) <- newCDCDecoder limits (Just emptyGtids) fixtureFormat "bin.000001" input+    S.toList output++wireEvent :: Bool -> BinLogPacket -> Packet+wireEvent crc p = Packet (L.length bytes) 1 bytes+  where+    raw = runPut $ do+        putWord32le (blTimestamp p) >> putWord8 (fromIntegral (fromEnum (blEventType p)))+        putWord32le (blServerId p) >> putWord32le (fromIntegral (19 + L.length (blBody p) + if crc then 4 else 0))+        putWord32le (fromIntegral (blLogPos p)) >> putWord16le (blFlags p) >> putLazyByteString (blBody p)+    bytes = runPut $ putWord8 0 >> putLazyByteString raw >> when crc (putWord32le (binLogCRC32 raw))++writes :: [CDCEvent] -> [[BinLogValue]]+writes events = concat [writeRowData row | event <- events, CDCWrite _ row <- [cdcEventPayload event]]++cases :: [Case]+cases =+    [ case5 "initial-format-budget" $ do+        input <- S.fromList []+        expectRejection (newCDCDecoder defaultDecodeLimits Nothing+            fixtureFormat {fdEventHeaderLenVector=B.replicate 257 8} "bin.1" input)+    , case5 "initial-filename-budget" $ do+        input <- S.fromList []+        expectRejection (newCDCDecoder defaultDecodeLimits Nothing fixtureFormat (B.replicate 513 65) input)+    , case5 "event-id-roundtrip" $ forM_ [0..255] $ \n -> equal "wire event ID" n (fromEnum (toEnum n :: BinLogEventType))+    , case5 "crc32-known-vector" $ equal "IEEE CRC32" 0xcbf43926 (binLogCRC32 "123456789")+    , case5 "crc32-valid" $ do+        result <- getFromPacket (getBinLogPacket True False) (wireEvent True (writeRow 1 0 7))+        equal "validated body" (blBody (writeRow 1 0 7)) (blBody result)+    , case5 "crc32-every-truncation" $ do+        let p = wireEvent True (writeRow 1 0 7)+        forM_ [0..L.length (pBody p)-1] $ \size -> expectRejection+            (getFromPacket (getBinLogPacket True False) p {pBody=L.take size (pBody p)})+    , case5 "wire-trailing-byte" $ do+        let p = wireEvent False (writeRow 1 0 7)+        expectRejection (getFromPacket (getBinLogPacket False False) p {pBody=pBody p <> "x"})+    , case5 "wire-small-event-size" $ expectRejection $ getFromPacket (getBinLogPacket False False)+        (putToPacket 1 (putWord8 0 >> putWord32le 0 >> putWord8 16 >> putWord32le 1 >> putWord32le 18 >> putWord32le 4 >> putWord16le 0))+    , case5 "unknown-critical-rejected" $ expectRejection (decode defaultDecodeLimits [packet (UnrecognizedBinLogEvent 200) (pure ())])+    , case5 "unknown-ignorable-preserved" $ do+        let original=(packet (UnrecognizedBinLogEvent 200) (putByteString "future")) {blFlags=0x80}+        result <- decode defaultDecodeLimits [original]+        equal "unknown bytes retained" [CDCIgnorable original] (map cdcEventPayload result)+    , case5 "table-map-replacement" $ do+        events <- decode defaultDecodeLimits [tableMap 1 [1],tableMap 1 [3],writeRow 1 0 7]+        equal "new schema metadata" [[BinLogLong 7]] (writes events)+    , case5 "table-map-count-limit" $ expectRejection (decode defaultDecodeLimits {maxTableMaps=1} [tableMap 1 [3],tableMap 2 [3]])+    , case5 "table-map-byte-limit" $ expectRejection (decode defaultDecodeLimits {maxTableMapBytes=1} [tableMap 1 [3]])+    , case5 "table-map-replacement-budget" $ do+        events <- decode defaultDecodeLimits {maxTableMaps=1,maxTableMapBytes=1000} [tableMap 1 [3],tableMap 1 [3],writeRow 1 0 7]+        equal "replacement releases metadata" [[BinLogLong 7]] (writes events)+    , case5 "statement-end-invalidates-maps" $ expectRejection (decode defaultDecodeLimits [tableMap 1 [3],writeRow 1 1 7,writeRow 1 0 8])+    , case5 "query-invalidates-maps" $ expectRejection (decode defaultDecodeLimits [tableMap 1 [3],queryEvent "ALTER TABLE t ADD n INT",writeRow 1 0 8])+    , case5 "rotate-invalidates-maps" $ expectRejection (decode defaultDecodeLimits+        [tableMap 1 [3],packet BINLOG_ROTATE_EVENT (putWord64le 4 >> putByteString "bin.000002"),writeRow 1 0 8])+    , case5 "row-post-header-length-rejected" $ do+        input <- S.fromList [tableMap 1 [3],writeRow 1 0 7]+        (events,_) <- newCDCDecoder defaultDecodeLimits (Just emptyGtids)+            fixtureFormat {fdEventHeaderLenVector=B.replicate 172 9} "bin.1" input+        expectRejection (S.toList events)+    , case5 "row-empty-images-rejected" $ expectRejection (decode defaultDecodeLimits+        [tableMap 1 [3],packet BINLOG_WRITE_ROWS_EVENTv1 (putWord48le 1 >> putWord16le 0 >> putLenEncInt 1 >> putWord8 1)])+    , case5 "row-column-count-mismatch" $ expectRejection (decode defaultDecodeLimits [tableMap 1 [3,3],writeRow 1 0 7])+    , case5 "row-extra-header-underflow" $ expectRejection (decode defaultDecodeLimits+        [tableMap 1 [3],packet BINLOG_WRITE_ROWS_EVENTv2 (putWord48le 1 >> putWord16le 0 >> putWord16le 1)])+    , case5 "row-zero-progress" $ expectRejection (decode defaultDecodeLimits+        [tableMap 1 [3],packet BINLOG_WRITE_ROWS_EVENTv1 (putWord48le 1 >> putWord16le 0 >> putLenEncInt 1 >> putWord8 0 >> putWord8 0)])+    , case5 "row-null-unused-bits-allowed" $ do+        let original=writeRow 1 0 7+            bytes=blBody original+            rowWithUnusedBits=original {blBody=L.take 10 bytes <> L.singleton 0xfe <> L.drop 11 bytes}+        events <- decode defaultDecodeLimits [tableMap 1 [3],rowWithUnusedBits]+        equal "server NULL padding is ignored" [[BinLogLong 7]] (writes events)+    , case5 "table-map-optional-metadata" $ do+        let original=tableMap 1 [3]+            withMetadata=original {blBody=blBody original <> runPut (putWord8 238 >> putLenEncBytes "abc")}+        events <- decode defaultDecodeLimits [withMetadata,writeRow 1 0 7]+        case map cdcEventPayload events of+            [CDCWrite table _] -> equal "unknown TLV preserved" [(238,"abc")] (tmOptionalMetadata table)+            _ -> fail "missing optional metadata"+    , case5 "table-map-invalid-optional-length" $ do+        let original=tableMap 1 [3]+        expectRejection (decode defaultDecodeLimits [original {blBody=blBody original <> L.pack [238,10,1,2,3]}])+    , case5 "file-checkpoint-resumes-by-file" $ do+        input <- S.fromList (transaction 1 7)+        (events,_) <- newCDCDecoder defaultDecodeLimits Nothing fixtureFormat "bin.1" input+        output <- S.toList events+        token <- only "file checkpoint" (mapMaybe cdcEventCheckpoint output)+        equal "unknown initial GTIDs are explicit" Nothing (checkpointGtids token)+        equal "resume preserves file origin" (FromBinLog (BinLogTracker "bin.1" 100)) (resumeFromCheckpoint token)+    , case5 "row-padding-bits-rejected" $ expectRejection (decode defaultDecodeLimits+        [tableMap 1 [3],packet BINLOG_WRITE_ROWS_EVENTv1 (putWord48le 1 >> putWord16le 0 >> putLenEncInt 1 >> putWord8 0x81 >> putWord8 0 >> putWord32le 7)])+    , case5 "sparse-update-images" $ do+        let row = packet BINLOG_UPDATE_ROWS_EVENTv1 $ do+                putWord48le 1 >> putWord16le 1 >> putLenEncInt 3+                putWord8 1 >> putWord8 6+                putWord8 0 >> putWord32le 7+                putWord8 1 >> putWord32le 9+        result <- decode defaultDecodeLimits [tableMap 1 [3,3,3],row]+        case map cdcEventPayload result of+            [CDCUpdate _ value] -> do+                equal "omitted differs from NULL" [([BinLogLong 7],[BinLogNull,BinLogLong 9])] (updateRowData value)+                equal "both sparse maps retained" (BitMap (B.pack [1]),BitMap (B.pack [6])) (updatePresentMap value)+            _ -> fail "missing sparse update"+    , case5 "checkpoint-only-after-commit" $ do+        events <- decode defaultDecodeLimits (transaction 1 7)+        equal "only commit checkpoint" [False,False,True] (map (maybe False (const True) . cdcEventCheckpoint) events)+        let tokens = mapMaybe cdcEventCheckpoint events+        equal "completed GTID" [Just [Gtid 0 610123 1]] (map (fmap gtidSetEntries . checkpointGtids) tokens)+    , case5 "checkpoint-rollback-boundary" $ do+        events <- decode defaultDecodeLimits [gtid 0 1 4,queryEvent "BEGIN",queryEvent "ROLLBACK"]+        equal "rollback emitted" CDCRollback (cdcEventPayload (last events))+        equal "one complete boundary" 1 (length (mapMaybe cdcEventCheckpoint events))+    , case5 "checkpoint-standalone-ddl" $ do+        events <- decode defaultDecodeLimits [gtid 0 1 1,queryEvent "CREATE TABLE t(n INT)"]+        equal "DDL boundary" 1 (length (mapMaybe cdcEventCheckpoint events))+    , case5 "checkpoint-standalone-rows" $ do+        events <- decode defaultDecodeLimits [gtid 0 1 1,tableMap 1 [3],writeRow 1 1 7]+        equal "nontransactional statement boundary" 1 (length (mapMaybe cdcEventCheckpoint events))+    , case5 "checkpoint-cross-domain" $ do+        events <- decode defaultDecodeLimits (transaction 1 7 ++ [gtid 2 9 4,packet BINLOG_XID_EVENT (putWord64le 2)])+        equal "both completed domains" (Just [Gtid 0 610123 1,Gtid 2 610123 9])+            (fmap gtidSetEntries (checkpointGtids (last (mapMaybe cdcEventCheckpoint events))))+    , case5 "checkpoint-incomplete-eof" $ expectRejection (decode defaultDecodeLimits [gtid 0 1 4,tableMap 1 [3],writeRow 1 1 7])+    , case5 "checkpoint-nested-gtid" $ expectRejection (decode defaultDecodeLimits [gtid 0 1 4,gtid 0 2 4])+    , case5 "checkpoint-nonadvancing-gtid" $ expectRejection (decode defaultDecodeLimits (transaction 2 7 ++ transaction 1 8))+    , case5 "checkpoint-xa-rejected" $ expectRejection (decode defaultDecodeLimits [gtid 0 1 64])+    , case5 "checkpoint-stream-identity" $ do+        first <- decode defaultDecodeLimits (transaction 1 7)+        second <- decode defaultDecodeLimits (transaction 1 7)+        a <- only "first checkpoint" (mapMaybe cdcEventCheckpoint first)+        b <- only "second checkpoint" (mapMaybe cdcEventCheckpoint second)+        assert (not (validCheckpoint a b)) "foreign token rejected"+    , case5 "gtid-parser-valid" $ do+        entries <- either fail pure (parseGtidSet "2-9-18446744073709551615,0-1-7")+        equal "canonical ordered state" "0-1-7,2-9-18446744073709551615" (renderGtidSet entries)+    , case5 "gtid-parser-invalid" $ forM_ ["0-1-0","0-1-1,0-2-2","-1-2-3","0-1-18446744073709551616","4294967296-1-2","0-1-2,"," 0-1-2"] $ \text ->+        case parseGtidSet text of Left _ -> pure (); Right _ -> fail "invalid GTID accepted"+    , case5 "backpressure-no-read-ahead" $ do+        count <- newIORef (0::Int)+        source <- S.fromList (transaction 1 7 ++ transaction 2 8)+        observed <- S.makeInputStream (modifyIORef' count (+1) >> S.read source)+        (events,latest) <- newCDCDecoder defaultDecodeLimits (Just emptyGtids) fixtureFormat "bin.1" observed+        void (S.read events)+        equal "one raw event per GTID read" 1 =<< readIORef count+        equal "no premature checkpoint" Nothing =<< latest+    , case5 "compressed-write" $ do+        let row = writeRow 1 0 7+        result <- decode defaultDecodeLimits [tableMap 1 [3], compressRow row]+        equal "inflated row" [[BinLogLong 7]] (writes result)+    , case5 "compressed-expansion-limit" $ expectRejection (decode defaultDecodeLimits {maxExpandedEventBytes=32}+        [tableMap 1 [3], (compressRow (writeRow 1 0 7)) {blBody= L.take 10 (blBody (writeRow 1 0 7)) <> L.pack [0x84,0x7f,0xff,0xff,0xff]}])+    , case5 "compressed-truncated" $ do+        let row=compressRow (writeRow 1 0 7)+        expectRejection (decode defaultDecodeLimits [tableMap 1 [3],row {blBody=L.init (blBody row)}])+    , case5 "compressed-length-mismatch" $ do+        let row=compressRow (writeRow 1 0 7)+            bytes=blBody row+        expectRejection (decode defaultDecodeLimits [tableMap 1 [3],row {blBody=L.take 11 bytes <> L.singleton 4 <> L.drop 12 bytes}])+    , case5 "fragmented-row" $ do+        events <- decode defaultDecodeLimits (tableMap 1 [3] : fragments (writeRow 1 0 7))+        equal "fragmented image" [[BinLogLong 7]] (writes events)+    , case5 "fragment-out-of-order" $ do+        let parts=fragments (writeRow 1 0 7)+        expectRejection (decode defaultDecodeLimits (tableMap 1 [3] : reverse parts))+    , case5 "fragment-missing" $ case fragments (writeRow 1 0 7) of+        first:_ -> expectRejection (decode defaultDecodeLimits [tableMap 1 [3],first])+        _ -> fail "invalid fragment fixture"+    , case5 "fragment-interleaved" $ case fragments (writeRow 1 0 7) of+        first:rest -> expectRejection (decode defaultDecodeLimits ([tableMap 1 [3],first,queryEvent "COMMIT"] ++ rest))+        _ -> fail "invalid fragment fixture"+    , case5 "fragment-expansion-budget" $ expectRejection+        (decode defaultDecodeLimits {maxExpandedEventBytes=32} (tableMap 1 [3] : fragments (writeRow 1 0 7)))+    , case5 "compressed-query" $ do+        let original=queryEvent "CREATE TABLE t(n INT)"+            prefix=L.take 14 (blBody original)+            sqlBytes=L.drop 14 (blBody original)+            compressed=original {blEventType=MARIADB_QUERY_COMPRESSED_EVENT,+                blBody=prefix <> L.pack [0x81,fromIntegral (L.length sqlBytes)] <> Z.compress sqlBytes}+        result <- decode defaultDecodeLimits [gtid 0 1 1,compressed]+        equal "compressed DDL checkpoint" 1 (length (mapMaybe cdcEventCheckpoint result))+    , case5 "set-little-endian" $ do+        value <- getFromBinLogPacket (getBinLogField (BINLOG_TYPE_SET 16 2)) (packet BINLOG_UNKNOWN_EVENT (putWord8 1 >> putWord8 2))+        equal "two-byte SET mask" (BinLogSet 513) value+    , case5 "enum-little-endian" $ do+        value <- getFromBinLogPacket (getBinLogField (BINLOG_TYPE_ENUM 2)) (packet BINLOG_UNKNOWN_EVENT (putWord8 1 >> putWord8 2))+        equal "two-byte ENUM index" (BinLogEnum 513) value+    , case5 "metadata-invalid-precision" $ expectRejection+        (getFromBinLogPacket (getBinLogMeta mySQLTypeNewDecimal) (packet BINLOG_UNKNOWN_EVENT (putWord8 0 >> putWord8 0)))+    , case5 "metadata-invalid-fsp" $ expectRejection+        (getFromBinLogPacket (getBinLogMeta mySQLTypeTimestamp2) (packet BINLOG_UNKNOWN_EVENT (putWord8 7)))+    , case5 "crc32-off-format-still-checked" $ do+        let fd=packet BINLOG_FORMAT_DESCRIPTION_EVENT $ do+                putWord16le 4 >> putByteString ("12.3.3-MariaDB" <> B.replicate 36 0)+                putWord32le 0 >> putWord8 19 >> putByteString (B.replicate 172 8) >> putWord8 0+        parsed <- getFromPacket (getBinLogPacket False False) (wireEvent True fd)+        format <- getFromBinLogPacket getFormatDescription parsed+        equal "descriptor excluded from event vector" 172 (B.length (fdEventHeaderLenVector format))+    , case5 "live-transaction-resume" gtidResumeFeature+    , case5 "manual-ack-wire-order" manualAckFeature+    , case5 "manual-ack-foreign-token" $ do+        (conn,_,_) <- ackFixture (transaction 1 7)+        stream <- openManual conn+        foreignEvents <- decode defaultDecodeLimits (transaction 1 7)+        token <- only "foreign token" (mapMaybe cdcEventCheckpoint foreignEvents)+        expectRejection (cdcAcknowledge stream token)+        equal "misuse preserves replication" Replicating =<< connectionStatus conn+        cdcClose stream+    , case5 "manual-ack-backwards" $ do+        (conn,_,_) <- ackFixture (transaction 1 7 ++ map (\p -> p {blLogPos=200}) (transaction 2 8))+        stream <- openManual conn+        first <- readCheckpoint stream+        second <- readCheckpoint stream+        cdcAcknowledge stream second+        expectRejection (cdcAcknowledge stream first)+        equal "backwards rejection preserves replication" Replicating =<< connectionStatus conn+        cdcClose stream+    , case5 "manual-ack-write-cancellation" $ do+        (original,_,closed) <- ackFixture (transaction 1 7)+        blocked <- newEmptyMVar+        let conn=original {C.mysqlWrite = \p -> if not (L.null (pBody p)) && L.head (pBody p)==0xef+                then takeMVar blocked else C.mysqlWrite original p}+        stream <- openManual conn+        token <- readCheckpoint stream+        equal "ACK write deadline" Nothing =<< timeout 20000 (cdcAcknowledge stream token)+        equal "interrupted ACK retires connection" Broken =<< connectionStatus conn+        equal "ACK cancellation releases once" 1 =<< closed+    , case5 "decoded-error-retires" $ do+        (conn,_,closed) <- ackFixture [tableMap 1 [3],writeRow 2 0 7]+        stream <- openManual conn+        expectRejection (S.read (cdcEvents stream))+        equal "semantic error retires connection" Broken =<< connectionStatus conn+        equal "semantic error releases transport" 1 =<< closed+    , case5 "decoded-error-cannot-be-skipped" $ do+        input <- S.fromList [packet (UnrecognizedBinLogEvent 200) (pure ()),queryEvent "COMMIT"]+        (events,latest) <- newCDCDecoder defaultDecodeLimits (Just emptyGtids) fixtureFormat "bin.1" input+        expectRejection (S.read events)+        expectRejection (S.read events)+        equal "no checkpoint after ignored exception" Nothing =<< latest+    , case5 "foreign-thread-read-rejected" $ do+        (conn,_,_) <- ackFixture (transaction 1 7)+        stream <- openManual conn+        done <- newEmptyMVar+        _ <- forkFinally (S.read (cdcEvents stream)) (putMVar done)+        outcome <- takeMVar done+        case outcome of+            Left err | Just ConcurrentConnectionUse <- fromException err -> pure ()+            _ -> fail "foreign thread accessed replication stream"+        equal "ownership violation preserves stream" Replicating =<< connectionStatus conn+        void (readCheckpoint stream)+        cdcClose stream+    , case5 "live-semisync-ack" liveSemiAck+    , case5 "live-rotation-schema-sparse" liveRotation+    , case5 "live-server-restart-resume" liveRestart+    , case5 "file-position-overflow-rejected" $ withConn $ \conn -> do+        expectRejection (openCDC conn 610126 (defaultCDCOptions (FromBinLog (BinLogTracker "bin.1" 0x100000000))))+        equal "pre-send validation leaves Ready" Ready =<< connectionStatus conn+    , case5 "live-query-excluded-during-cdc" $ withConn $ \conn -> do+        tracker <- getLastBinLogTracker conn >>= maybe (fail "no binlog") pure+        stream <- openCDC conn 610126 (defaultCDCOptions (FromBinLog tracker))+        expectRejection (query_ conn "SELECT 1")+        equal "replication lease intact" Replicating =<< connectionStatus conn+        cdcClose stream+    , case5 "live-read-cancellation-retires" $ withConn $ \conn -> do+        tracker <- getLastBinLogTracker conn >>= maybe (fail "no binlog") pure+        stream <- openCDC conn 610126 (defaultCDCOptions (FromBinLog tracker))+        -- Drain any injected GTID_LIST first, then block on a quiet source.+        result <- timeout 100000 (forever (void (S.read (cdcEvents stream))) :: IO ())+        equal "deadline reached" Nothing result+        equal "cancelled replication retired" Broken =<< connectionStatus conn+    ] ++ [case5 ("compressed-row-variant-" ++ show (fromEnum compressed)) $ do+        let version2=normal `elem` [BINLOG_WRITE_ROWS_EVENTv2,BINLOG_UPDATE_ROWS_EVENTv2,BINLOG_DELETE_ROWS_EVENTv2]+            update=normal `elem` [BINLOG_UPDATE_ROWS_EVENTv1,BINLOG_UPDATE_ROWS_EVENTv2]+            original=packet normal $ do+                putWord48le 1 >> putWord16le 0+                when version2 (putWord16le 2)+                putLenEncInt 1 >> putWord8 1+                when update (putWord8 1)+                putWord8 0 >> putWord32le 7+                when update (putWord8 0 >> putWord32le 8)+            offset=10 + (if version2 then 2 else 0) + (if update then 1 else 0)+            compressedEvent=original {blEventType=compressed,blBody=L.take offset (blBody original)+                <> L.pack [0x81,fromIntegral (L.length (blBody original)-offset)] <> Z.compress (L.drop offset (blBody original))}+        result <- decode defaultDecodeLimits [tableMap 1 [3],compressedEvent]+        case map cdcEventPayload result of+            [CDCWrite _ value] -> equal "compressed insert" [[BinLogLong 7]] (writeRowData value)+            [CDCDelete _ value] -> equal "compressed delete" [[BinLogLong 7]] (deleteRowData value)+            [CDCUpdate _ value] -> equal "compressed update" [([BinLogLong 7],[BinLogLong 8])] (updateRowData value)+            _ -> fail "missing compressed variant"+    | (compressed,normal) <-+        [(MARIADB_WRITE_ROWS_COMPRESSED_EVENTv1,BINLOG_WRITE_ROWS_EVENTv1),+         (MARIADB_UPDATE_ROWS_COMPRESSED_EVENTv1,BINLOG_UPDATE_ROWS_EVENTv1),+         (MARIADB_DELETE_ROWS_COMPRESSED_EVENTv1,BINLOG_DELETE_ROWS_EVENTv1),+         (MARIADB_WRITE_ROWS_COMPRESSED_EVENTv2,BINLOG_WRITE_ROWS_EVENTv2),+         (MARIADB_UPDATE_ROWS_COMPRESSED_EVENTv2,BINLOG_UPDATE_ROWS_EVENTv2),+         (MARIADB_DELETE_ROWS_COMPRESSED_EVENTv2,BINLOG_DELETE_ROWS_EVENTv2)]] ++ [case5 ("bitmap-width-" ++ show width) $ do+        let row=packet BINLOG_WRITE_ROWS_EVENTv1 $ do+                putWord48le 1 >> putWord16le 0 >> putLenEncInt width+                putByteString (B.pack (replicate (width `div` 8) 255 ++ [fromIntegral ((1 `shiftL` (width `mod` 8)) - 1 :: Int) | width `mod` 8 /= 0]))+                putByteString (B.replicate ((width+7) `div` 8) 0)+                replicateM_ width (putWord32le 7)+        events <- decode defaultDecodeLimits [tableMap 1 (replicate width 3),row]+        equal "all columns survive bitmap" [replicate width (BinLogLong 7)] (writes events)+    | width <- [1,7,8,9,15,16,17]]++compressRow :: BinLogPacket -> BinLogPacket+compressRow row = row { blEventType=MARIADB_WRITE_ROWS_COMPRESSED_EVENTv1,+    blBody=L.take 10 (blBody row) <> L.pack [0x81,5] <> Z.compress (L.drop 10 (blBody row)) }++-- Real source transactions are committed before a nonblocking dump. Resume from+-- the first delivered checkpoint must return the second transaction exactly once+-- in this controlled log (application crash recovery may legitimately replay).+gtidResumeFeature :: IO ()+gtidResumeFeature = withConn $ \writer -> do+    exec writer "DROP TABLE IF EXISTS mb_stage5_resume"+    exec writer "CREATE TABLE mb_stage5_resume(id INT PRIMARY KEY,n INT) ENGINE=InnoDB"+    (_,stateRows) <- query_ writer "SELECT @@global.gtid_binlog_pos"+    state <- S.toList stateRows+    start <- case state of+        [[MySQLText text]] -> either fail pure (parseGtidSet (TE.encodeUtf8 text))+        _ -> fail "missing GTID position"+    withTransaction writer $ exec writer "INSERT INTO mb_stage5_resume VALUES(1,7)"+    withTransaction writer $ exec writer "INSERT INTO mb_stage5_resume VALUES(2,8)"+    withConn $ \source -> do+        stream <- openCDC source 610126 (defaultCDCOptions (FromGtid start)) {cdcNonBlocking=True}+        first <- untilCheckpoint stream+        equal "first complete transaction" [[BinLogLong 1,BinLogLong 7]] (writes first)+        checkpoint <- maybe (fail "missing first checkpoint") pure (cdcEventCheckpoint (last first))+        cdcClose stream+        withConn $ \resumed -> do+            next <- openCDC resumed 610127 (defaultCDCOptions (resumeFromCheckpoint checkpoint)) {cdcNonBlocking=True}+            result <- S.toList (cdcEvents next)+            equal "GTID resume returns only unprocessed transaction" [[BinLogLong 2,BinLogLong 8]] (writes result)+  where+    untilCheckpoint stream = do+        value <- S.read (cdcEvents stream) >>= maybe (fail "EOF before checkpoint") pure+        if maybe False (const True) (cdcEventCheckpoint value) then pure [value]+        else (value:) <$> untilCheckpoint stream++-- Literal semisync transcript observes actual client writes. Neither GTID nor+-- row delivery may send an ACK; only a completed consumer checkpoint may do so.+manualAckFeature :: IO ()+manualAckFeature = do+    (conn,sent,_) <- ackFixture (transaction 1 7)+    stream <- openCDC conn 610126 (defaultCDCOptions (FromBinLog (BinLogTracker "bin.000001" 4))) {cdcAcknowledgements=AcknowledgeManually}+    let ackPackets = filter (\p -> not (L.null (pBody p)) && L.head (pBody p)==0xef)+    equal "no bootstrap ACK" 0 . length . ackPackets =<< sent+    begin <- S.read (cdcEvents stream)+    assert (maybe False (const True) begin) "GTID delivered"+    void (S.read (cdcEvents stream))+    equal "no row ACK before durability checkpoint" 0 . length . ackPackets =<< sent+    committed <- S.read (cdcEvents stream) >>= maybe (fail "missing commit") pure+    token <- maybe (fail "missing checkpoint") pure (cdcEventCheckpoint committed)+    cdcAcknowledge stream token+    acks <- ackPackets <$> sent+    equal "one explicit ACK" 1 (length acks)+    acknowledgement <- only "one ACK packet" acks+    equal "exact ACK wire bytes" (runPut (putWord8 0xef >> putWord64le 100 >> putByteString "bin.000001")) (pBody acknowledgement)+    cdcAcknowledge stream token+    equal "repeat acknowledgement is idempotent" 1 . length . ackPackets =<< sent+    cdcClose stream+++-- This one check owns a disposable socket-only server. Its semisync counter+-- independently proves that a writer waits until our consumer acknowledges.+liveSemiAck :: IO ()+liveSemiAck = do+    fixture <- lookupEnv "MB_CDC_SOCKET"+    case fixture of+        Nothing -> do+            binary <- getExecutablePath+            callProcess "python3" ["validation/cdc_fixture.py",binary,"case","stage5.live-semisync-ack"]+        Just path -> do+            let open = connectUnixSocket path defaultConnectInfo {ciUser="root",ciPassword="",ciDatabase="cdc_validation"}+            withConnect open $ \source -> do+                tracker <- getLastBinLogTracker source >>= maybe (fail "no fixture binlog") pure+                stream <- openCDC source 610128 (defaultCDCOptions (FromBinLog tracker)) {cdcAcknowledgements=AcknowledgeManually}+                done <- newEmptyMVar+                bracket (forkFinally (withConnect open (\writer -> exec writer "INSERT INTO changes VALUES(1)")) (putMVar done))+                    killThread $ \_ -> do+                        let nextCommit = do+                                event <- S.read (cdcEvents stream) >>= maybe (fail "EOF before live commit") pure+                                maybe nextCommit pure (cdcEventCheckpoint event)+                        token <- timeout 2000000 nextCommit >>= maybe (fail "no semisync transaction") pure+                        pending <- tryReadMVar done+                        case pending of+                            Nothing -> pure ()+                            Just (Left err) -> throwIO err+                            Just (Right ()) -> fail "writer completed before explicit acknowledgement"+                        cdcAcknowledge stream token+                        result <- timeout 2000000 (takeMVar done) >>= maybe (fail "writer did not resume after ACK") pure+                        either throwIO pure result+                        cdcClose stream+                withConnect open $ \observer -> do+                    (_,statusRows) <- query_ observer "SHOW GLOBAL STATUS LIKE 'Rpl_semi_sync_master_yes_tx'"+                    result <- S.toList statusRows+                    case result of+                        [[_,MySQLText "1"]] -> pure ()+                        _ -> fail ("server did not confirm semisync ACK: " ++ show result)+++only :: String -> [a] -> IO a+only _ [value] = pure value+only label _ = fail (label ++ ": expected one value")++fragments :: BinLogPacket -> [BinLogPacket]+fragments event =+    [packet MARIADB_PARTIAL_ROW_DATA_EVENT (putWord32le 2 >> putWord32le 1 >> putWord8 1 >> putWord64le (fromIntegral (L.length raw)) >> putLazyByteString (L.take 21 raw)),+     packet MARIADB_PARTIAL_ROW_DATA_EVENT (putWord32le 2 >> putWord32le 2 >> putWord8 0 >> putLazyByteString (L.drop 21 raw))]+  where raw=L.drop 1 (pBody (wireEvent False event))+++ackFixture :: [BinLogPacket] -> IO (MySQLConn,IO [Packet],IO Int)+ackFixture events = do+    let variable name value =+            [putToPacket 1 (putLenEncInt 2)] +++            [encodeToPacket n (ColumnDef "" "" "" "v" "" 33 100 mySQLTypeVarString 0 0) | n <- [2,3]] +++            [encodeToPacket 4 (EOF 0 2),putToPacket 5 (putLenEncBytes name >> putLenEncBytes value),encodeToPacket 6 (EOF 0 2)]+        fd = packet BINLOG_FORMAT_DESCRIPTION_EVENT $ do+            putWord16le 4 >> putByteString ("12.3.3-MariaDB" <> B.replicate 36 0)+            putWord32le 0 >> putWord8 19 >> putByteString (B.replicate 172 8) >> putWord8 1+        asSemi event =+            let base = wireEvent True event+                body = L.take 1 (pBody base) <> L.pack [0xef,1] <> L.drop 1 (pBody base)+            in base {pLen=L.length body,pBody=body}+        transcript = variable "rpl_semi_sync_master_enabled" "ON"+            ++ [okPacket 1]+            ++ map asSemi ([packet BINLOG_ROTATE_EVENT (putWord64le 4 >> putByteString "bin.000001"),fd] ++ events)+    scripted defaultConnectionOptions transcript++openManual :: MySQLConn -> IO CDCStream+openManual conn = openCDC conn 610126 (defaultCDCOptions (FromBinLog (BinLogTracker "bin.000001" 4)))+    {cdcAcknowledgements=AcknowledgeManually}++readCheckpoint :: CDCStream -> IO CDCCheckpoint+readCheckpoint stream = do+    event <- S.read (cdcEvents stream) >>= maybe (fail "EOF before expected checkpoint") pure+    maybe (readCheckpoint stream) pure (cdcEventCheckpoint event)+++withIsolated :: String -> (IO MySQLConn -> IO ()) -> IO ()+withIsolated ident action = do+    path <- lookupEnv "MB_CDC_SOCKET"+    case path of+        Nothing -> do+            binary <- getExecutablePath+            callProcess "python3" ["validation/cdc_fixture.py",binary,"case",ident]+        Just socket -> action (connectUnixSocket socket defaultConnectInfo+            {ciUser="root",ciPassword="",ciDatabase="cdc_validation"})++sourceGtids :: MySQLConn -> IO GtidSet+sourceGtids conn = do+    (_,input) <- query_ conn "SELECT @@global.gtid_binlog_pos"+    result <- S.toList input+    case result of+        [[MySQLText value]] -> either fail pure (parseGtidSet (TE.encodeUtf8 value))+        _ -> fail "missing source GTID state"++-- Cross a real rotation and CRC32->NONE transition, change schema, and decode+-- minimal before/after/delete images without interpreting omitted fields as NULL.+liveRotation :: IO ()+liveRotation = withIsolated "stage5.live-rotation-schema-sparse" $ \open -> withConnect open $ \writer -> do+    exec writer "SET GLOBAL rpl_semi_sync_master_enabled=0"+    start <- sourceGtids writer+    exec writer "INSERT INTO changes VALUES(1)"+    exec writer "SET GLOBAL binlog_checksum='NONE'"+    exec writer "FLUSH LOGS"+    exec writer "INSERT INTO changes VALUES(2)"+    exec writer "ALTER TABLE changes ADD n INT NOT NULL DEFAULT 7"+    exec writer "SET SESSION binlog_row_image='MINIMAL'"+    exec writer "UPDATE changes SET n=8 WHERE id=2"+    exec writer "DELETE FROM changes WHERE id=2"+    finalPosition <- getLastBinLogTracker writer >>= maybe (fail "no final binlog position") pure+    withConnect open $ \source -> do+        stream <- openCDC source 610128 (defaultCDCOptions (FromGtid start)) {cdcNonBlocking=True}+        result <- S.toList (cdcEvents stream)+        equal "rows across rotation" [[BinLogLong 1],[BinLogLong 2]] (writes result)+        let updates=[updateRowData value | event <- result,CDCUpdate _ value <- [cdcEventPayload event]]+            deletes=[deleteRowData value | event <- result,CDCDelete _ value <- [cdcEventPayload event]]+            rotations=[value | event <- result,CDCRotate value <- [cdcEventPayload event]]+        equal "minimal update images" [[([BinLogLong 2],[BinLogLong 8])]] updates+        equal "minimal delete image" [[[BinLogLong 2]]] deletes+        assert (not (null rotations)) "real rotation delivered"+        case reverse (mapMaybe cdcEventCheckpoint result) of+            token:_ -> equal "checkpoint moved to rotated file" (btFileName finalPosition) (binlogFile (checkpointPosition token))+            _ -> fail "no final checkpoint across rotation"++-- Restart the owned server with the same datadir after a complete checkpoint.+-- The old transport must become terminal and GTID resume must yield only the+-- subsequent transaction. The Python fixture owns every server process.+liveRestart :: IO ()+liveRestart = withIsolated "stage5.live-server-restart-resume" $ \open -> do+    start <- withConnect open $ \writer -> do+        exec writer "SET GLOBAL rpl_semi_sync_master_enabled=0"+        state <- sourceGtids writer+        exec writer "INSERT INTO changes VALUES(1)"+        pure state+    saved <- withConnect open $ \source -> do+        stream <- openCDC source 610128 (defaultCDCOptions (FromGtid start))+        token <- readCheckpoint stream+        directory <- lookupEnv "MB_CDC_CONTROL" >>= maybe (fail "missing fixture control directory") pure+        writeFile (directory </> "restart.request") "restart owned fixture\n"+        let ready = do+                done <- doesFileExist (directory </> "restart.done")+                unless done (threadDelay 20000 >> ready)+        timeout 15000000 ready >>= maybe (fail "server restart deadline") pure+        let untilFailure = do+                event <- S.read (cdcEvents stream)+                case event of Nothing -> fail "old replication ended"; Just _ -> untilFailure+        expectRejection (timeout 2000000 untilFailure >>= maybe (fail "old transport did not terminate") pure)+        status <- connectionStatus source+        assert (status `elem` [Broken,Closed]) "old transport is terminal after server restart"+        pure token+    withConnect open $ \writer -> exec writer "INSERT INTO changes VALUES(2)"+    withConnect open $ \source -> do+        stream <- openCDC source 610128 (defaultCDCOptions (resumeFromCheckpoint saved)) {cdcNonBlocking=True}+        result <- S.toList (cdcEvents stream)+        equal "restart resumes after saved transaction" [[BinLogLong 2]] (writes result)
+ validation/hs/Validation/Stage5Bench.hs view
@@ -0,0 +1,168 @@+{-# LANGUAGE BangPatterns #-}+-- | Streaming CDC workloads. Fixture encoding is outside decoder timing; the+-- framing/CRC profile deliberately includes encoding fresh, varying messages.+-- Live profiles include connect/setup, decoding and checkpoint observation, with+-- source log creation outside timing. All row values contribute to a checksum.+module Validation.Stage5Bench (run) where++import Control.Exception (evaluate)+import Control.Monad+import qualified Codec.Compression.Zlib as Z+import Data.Binary.Put+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as L+import Data.IORef+import Data.List (intercalate)+import qualified Data.Text.Encoding as TE+import Data.Word+import Database.MySQL.Base+import Database.MySQL.BinLog+import GHC.Clock+import GHC.Stats+import System.Mem+import qualified System.IO.Streams as S+import Validation.Common++run :: String -> Int -> IO ()+run ident count+    | count <= 0 = fail "positive CDC benchmark count required"+    | ident == "cdc.decode.single-table" = measure ident count $ decodeRows+        (mapping 1 : [row 1 n | n <- [1..count]])+    | ident == "cdc.decode.interleaved" = measure ident count $ decodeRows+        ([mapping tid | tid <- [1..64]] ++ [row (fromIntegral (n `mod` 64 + 1)) n | n <- [1..count]])+    | ident == "cdc.decode.sparse-update" = measure ident count $ do+        let metadata=make BINLOG_TABLE_MAP_EVENT $ do+                putWord48le 1 >> putWord16le 0+                putWord8 1 >> putByteString "d" >> putWord8 0+                putWord8 1 >> putByteString "t" >> putWord8 0+                putLenEncInt 64 >> putByteString (B.replicate 64 3) >> putLenEncInt 0 >> putWord64le 0+            update n = make BINLOG_UPDATE_ROWS_EVENTv1 $ do+                putWord48le 1 >> putWord16le 0 >> putLenEncInt 64+                putWord64le 1 >> putWord64le 0x8000000000000000+                putWord8 0 >> putWord32le (fromIntegral (negate n))+                putWord8 0 >> putWord32le (fromIntegral n)+        decodeRows (metadata : map update [1..count])+    | ident == "cdc.decode.compressed" = measure ident count $ decodeRows+        (mapping 1 : [compressedRow n | n <- [1..count]])+    | ident == "cdc.framing.crc32" = measure ident count $ do+        total <- foldM (\ !acc n -> do+            let event=row 1 n+                raw=runPut $ do+                    putWord32le 0 >> putWord8 23 >> putWord32le 1+                    putWord32le (fromIntegral (23 + L.length (blBody event))) >> putWord32le 100 >> putWord16le 0+                    putLazyByteString (blBody event)+                framed=putToPacket 1 (putWord8 0 >> putLazyByteString raw >> putWord32le (binLogCRC32 raw))+            checked <- getFromPacket (getBinLogPacket True False) framed+            unless (blBody checked == blBody event) (fail "CRC fixture roundtrip")+            pure (acc + 1)) (0 :: Integer) [1..count]+        pure total+    | ident == "cdc.retention" = measure ident count $ do+        cursor <- newIORef (0 :: Int)+        input <- S.makeInputStream $ do+            n <- atomicModifyIORef' cursor (\value -> (value+1,value))+            pure (if n==0 then Just (mapping 1) else if n<=count then Just (row 1 n) else Nothing)+        gtids <- either fail pure (gtidSet [])+        (events,_) <- newCDCDecoder defaultDecodeLimits (Just gtids) fixtureFormat "bin.1" input+        let loop :: Int -> Integer -> IO Integer+            loop !n !total = do+                event <- S.read events+                case event of+                    Nothing -> pure total+                    Just value -> do+                        addition <- case cdcEventPayload value of+                            CDCWrite _ image -> case writeRowData image of+                                [[BinLogLong valueNumber]] -> pure (toInteger valueNumber)+                                _ -> fail "retention image mismatch"+                            _ -> fail "retention event mismatch"+                        let next=n+1+                        when (next `elem` [1000,10000,100000,1000000]) $ do+                            performGC+                            stats <- getRTSStats+                            putStrLn (intercalate "\t" ["STAGE5_HEAP",show next,show (gcdetails_live_bytes (gc stats))])+                        loop next (total+addition)+        loop 0 0+    | ident == "cdc.live.rows" || ident == "cdc.live.checkpoints" = withConn $ \writer -> do+        exec writer "DROP TABLE IF EXISTS mb_stage5_bench"+        exec writer "CREATE TABLE mb_stage5_bench(id INT PRIMARY KEY,n INT) ENGINE=InnoDB"+        (_,stateStream) <- query_ writer "SELECT @@global.gtid_binlog_pos"+        state <- S.toList stateStream+        start <- case state of+            [[MySQLText value]] -> either fail pure (parseGtidSet (TE.encodeUtf8 value))+            _ -> fail "CDC benchmark GTID state"+        if ident == "cdc.live.rows" then withTransaction writer $ insertRows writer count+        else forM_ [1..count] $ \n -> withTransaction writer $ void $ execute writer+            "INSERT INTO mb_stage5_bench VALUES(?,1)" [MySQLInt32 (fromIntegral n)]+        measure ident count $ withConn $ \source -> do+            stream <- openCDC source 610130 (defaultCDCOptions (FromGtid start)) {cdcNonBlocking=True}+            (checksum,checkpoints) <- consume (cdcEvents stream)+            equal "live checkpoint count" (if ident=="cdc.live.rows" then 1 else count) checkpoints+            pure checksum+    | otherwise = fail "unknown CDC benchmark profile"+  where+    insertRows conn n = forM_ [1..n] $ \key -> void $ execute conn+        "INSERT INTO mb_stage5_bench VALUES(?,1)" [MySQLInt32 (fromIntegral key)]++fixtureFormat :: FormatDescription+fixtureFormat = FormatDescription 4 "12.3.3-MariaDB" 0 (B.replicate 172 8)++make :: BinLogEventType -> Put -> BinLogPacket+make typ builder = BinLogPacket 0 typ 1 (fromIntegral (19+L.length body)) 100 0 body False+  where body=runPut builder++mapping :: Word64 -> BinLogPacket+mapping tid = make BINLOG_TABLE_MAP_EVENT $ do+    putWord48le tid >> putWord16le 0+    putWord8 1 >> putByteString "d" >> putWord8 0+    putWord8 1 >> putByteString "t" >> putWord8 0+    putLenEncInt 1 >> putWord8 3 >> putLenEncInt 0 >> putWord8 0++row :: Word64 -> Int -> BinLogPacket+row tid n = make BINLOG_WRITE_ROWS_EVENTv1 $ do+    putWord48le tid >> putWord16le 0 >> putLenEncInt 1 >> putWord8 1 >> putWord8 0+    putWord32le (fromIntegral n)++compressedRow :: Int -> BinLogPacket+compressedRow n = let event=row 1 n in event {blEventType=MARIADB_WRITE_ROWS_COMPRESSED_EVENTv1,+    blBody=L.take 10 (blBody event) <> L.pack [0x81,5] <> Z.compress (L.drop 10 (blBody event))}++decodeRows :: [BinLogPacket] -> IO Integer+decodeRows packets = do+    input <- S.fromList packets+    gtids <- either fail pure (gtidSet [])+    (events,_) <- newCDCDecoder defaultDecodeLimits (Just gtids) fixtureFormat "bin.1" input+    fst <$> consume events++consume :: S.InputStream CDCEvent -> IO (Integer,Int)+consume input = loop 0 0+  where+    loop !total !checkpoints = do+        value <- S.read input+        case value of+            Nothing -> pure (total,checkpoints)+            Just event -> do+                let count=case cdcEventPayload event of+                        CDCWrite _ rowsEvent -> sum [toInteger n | image <- writeRowData rowsEvent,+                            BinLogLong n <- [last image]]+                        CDCUpdate _ updateEvent -> sum [toInteger after | ([BinLogLong before],[BinLogLong after]) <- updateRowData updateEvent,+                            before == negate after]+                        _ -> 0+                    commits=maybe 0 (const 1) (cdcEventCheckpoint event)+                loop (total+count) (checkpoints+commits)++measure :: String -> Int -> IO Integer -> IO ()+measure ident units operation = do+    void operation+    void operation+    performGC+    before <- getRTSStats+    start <- getMonotonicTimeNSec+    checksum <- operation >>= evaluate+    end <- getMonotonicTimeNSec+    performGC+    after <- getRTSStats+    let expected | ident `elem` ["cdc.decode.single-table","cdc.decode.interleaved","cdc.decode.compressed","cdc.decode.sparse-update","cdc.retention"] = toInteger units*(toInteger units+1) `div` 2+                 | otherwise = toInteger units+    equal "CDC benchmark checksum" expected checksum+    putStrLn (intercalate "\t" ["STAGE5",ident,show units,show (end-start),+        show (allocated_bytes after-allocated_bytes before),show (cpu_ns after-cpu_ns before),+        show (gc_cpu_ns after-gc_cpu_ns before),show checksum])
+ validation/hs/Validation/Stage5Review.hs view
@@ -0,0 +1,256 @@+{-# LANGUAGE ScopedTypeVariables #-}+-- | Follow-up regressions encode independent server-format literals and use+-- disposable MariaDB instances for temporal values, origin changes and DDL.+module Validation.Stage5Review (cases) where++import Control.Exception hiding (assert)+import Control.Monad+import Data.Binary.Put+import Data.Bits+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as L+import Data.IORef+import Data.List (intercalate)+import Data.Word+import Database.MySQL.Base+import Database.MySQL.BinLog+import qualified Database.MySQL.Connection as C+import qualified System.IO.Streams as S+import Validation.Common+import Validation.Stage5 (ackFixture, decode, fixtureFormat, packet,+    sourceGtids, tableMap, transaction, withIsolated)++review :: String -> IO () -> Case+review name = Case ("stage5.review-" ++ name) "stage5"++-- MariaDB stores fractional seconds in pairs of decimal digits. Odd precision+-- still uses the same byte scale as the following even precision (compat56.cc).+fractions :: [(Word8,[Word8],Word32)]+fractions = [(0,[],0),(1,[10],100000),(2,[12],120000),+    (3,[0x04,0xce],123000),(4,[0x04,0xd2],123400),+    (5,[0x01,0xe2,0x3a],123450),(6,[0x01,0xe2,0x40],123456)]++field :: BinLogMeta -> [Word8] -> IO BinLogValue+field meta bytes = getFromBinLogPacket (getBinLogField meta)+    (packet BINLOG_UNKNOWN_EVENT (putByteString (B.pack bytes)))++firstEvent :: L.ByteString -> IO (Maybe CDCEvent)+firstEvent body = do+    input <- S.fromList [(packet MARIADB_GTID_EVENT (pure ())) {blBody=body}]+    empty <- either fail pure (gtidSet [])+    (events,_) <- newCDCDecoder defaultDecodeLimits (Just empty) fixtureFormat "bin.1" input+    S.read events++gtidPrefix :: Word8 -> Put+gtidPrefix flags = putWord64le 1 >> putWord32le 0 >> putWord8 flags++expectUnsupported :: IO a -> IO ()+expectUnsupported action = do+    result <- try action+    case result of+        Left (CDCUnsupportedFeature _) -> pure ()+        Left other -> throwIO (other :: CDCError)+        Right _ -> fail "unsafe GTID extension was accepted"++cases :: [Case]+cases = concat+    [ [review ("timestamp-fsp-" ++ show precision) $ equal "timestamp microseconds"+            (BinLogTimeStamp2 0x01020304 micros) =<< field (BINLOG_TYPE_TIMESTAMP2 precision)+                ([1,2,3,4] ++ bytes),+       review ("datetime-fsp-" ++ show precision) $ equal "datetime microseconds"+            (BinLogDateTime2 2026 1 2 3 4 5 micros) =<< field (BINLOG_TYPE_DATETIME2 precision)+                ([0x99,0xb8,0xc4,0x31,0x05] ++ bytes),+       review ("time-positive-fsp-" ++ show precision) $ equal "positive time"+            (BinLogTime2 1 1 2 3 micros) =<< field (BINLOG_TYPE_TIME2 precision)+                ([0x80,0x10,0x83] ++ bytes),+       review ("time-negative-fsp-" ++ show precision) $ do+            -- A nonzero fraction borrows one unit from the integer part. The+            -- zero-fraction encoding below deliberately has no such borrow.+            let prefix = if micros == 0 then [0x7f,0xef,0x7d] else [0x7f,0xef,0x7c]+            equal "negative time" (BinLogTime2 0 1 2 3 micros) =<<+                field (BINLOG_TYPE_TIME2 precision) (prefix ++ negative),+       review ("time-negative-whole-fsp-" ++ show precision) $ equal "negative whole seconds"+            (BinLogTime2 0 1 2 3 0) =<< field (BINLOG_TYPE_TIME2 precision)+                ([0x7f,0xef,0x7d] ++ replicate (length bytes) 0)]+    | ((precision,bytes,micros),negative) <- zip fractions+        [[],[0xf6],[0xf4],[0xfb,0x32],[0xfb,0x2e],[0xfe,0x1d,0xc6],[0xfe,0x1d,0xc0]]] +++    [ review "gtid-list-multiple-origins" $ do+        let history=packet MARIADB_GTID_LIST_EVENT $ do+                putWord32le 2+                putWord32le 0 >> putWord32le 11 >> putWord64le 7+                putWord32le 0 >> putWord32le 12 >> putWord64le 8+        events <- decode defaultDecodeLimits [history]+        case map cdcEventPayload events of+            [CDCGtidList value] -> do+                equal "all origin history retained" [Gtid 0 11 7,Gtid 0 12 8] (gtidListEntries value)+                equal "history flags" 0 (gtidListFlags value)+            _ -> fail "missing GTID history"+    , review "gtid-list-flags-preserved" $ do+        events <- decode defaultDecodeLimits [packet MARIADB_GTID_LIST_EVENT (putWord32le 0x20000000)]+        equal "ignored-GTID flag preserved" [CDCGtidList (GtidList 0x20000000 [])] (map cdcEventPayload events)+    , review "gtid-list-duplicate-pair-rejected" $ expectRejection $ decode defaultDecodeLimits+        [packet MARIADB_GTID_LIST_EVENT $ do+            putWord32le 2+            replicateM_ 2 (putWord32le 0 >> putWord32le 11 >> putWord64le 7)]+    , review "gtid-list-entry-budget" $ expectRejection $ decode defaultDecodeLimits+        [packet MARIADB_GTID_LIST_EVENT (putWord32le 4097)]+    , review "gtid-short-header" $ forM_ [13..18] $ \size ->+        expectRejection (firstEvent (L.take size (runPut (gtidPrefix 4 >> putWord64le 0))))+    , review "gtid-truncated-engine-extension" $ expectRejection $ firstEvent $ runPut $ do+        gtidPrefix 2 >> putWord64le 3 >> putWord8 1+    , review "gtid-truncated-thread-extension" $ expectRejection $ firstEvent $ runPut $ do+        gtidPrefix 2 >> putWord64le 3 >> putWord8 16 >> putWord8 1+    , review "gtid-known-extensions" $ do+        value <- firstEvent $ runPut $ do+            gtidPrefix 2 >> putWord64le 3 >> putWord8 17 >> putWord8 1 >> putWord32le 9+        assert (maybe False (const True) value) "valid commit/engine/thread extensions rejected"+    , review "gtid-two-phase-alter-rejected" $ forM_ [2,4,8] $ \flags ->+        expectUnsupported $ firstEvent $ runPut $ do+            gtidPrefix 1 >> putWord8 flags+            when (flags .&. 12 /= 0) (putWord64le 1)+            putByteString (B.replicate 5 0)+    , review "gtid-unknown-extension-rejected" $ expectUnsupported $ firstEvent $ runPut $ do+        gtidPrefix 1 >> putWord8 32 >> putByteString (B.replicate 5 0)+    , review "invalid-temporal-fraction-rejected" $ do+        expectRejection (field (BINLOG_TYPE_TIMESTAMP2 1) [1,2,3,4,11])+        expectRejection (field (BINLOG_TYPE_TIMESTAMP2 6) [1,2,3,4,255,255,255])+        expectRejection (field (BINLOG_TYPE_DATETIME2 4) [0x99,0xb8,0xc4,0x31,0x05,0x27,0x10])+        expectRejection (field (BINLOG_TYPE_TIME2 2) [0x80,0x10,0x83,255])+    , review "time-negative-fraction-only" $ equal "negative subsecond time"+        (BinLogTime2 0 0 0 0 1) =<< field (BINLOG_TYPE_TIME2 6) [0x7f,0xff,0xff,255,255,255]+    , review "large-query-control-prefix" $ forM_ ["BEGIN","COMMIT","ROLLBACK"] $ \prefix -> do+        let statement=B.replicate 16384 32 <> prefix <> " " <> B.replicate (1024*1024) 120+            query=packet BINLOG_QUERY_EVENT $ do+                putWord32le 0 >> putWord32le 0 >> putWord8 0+                putWord16le 0 >> putWord16le 0 >> putWord8 0 >> putByteString statement+        case transaction 1 7 of+            [begin,_,_,commit] -> do+                events <- decode defaultDecodeLimits [begin,query,commit]+                equal "prefix is not a transaction boundary" [False,False,True]+                    (map (maybe False (const True) . cdcEventCheckpoint) events)+                case map cdcEventPayload events of+                    [CDCBegin _ _,CDCQuery value,CDCCommit _] ->+                        equal "complete query is retained" (Query (L.fromStrict statement)) (qQuery value)+                    _ -> fail "large query was misclassified"+            _ -> fail "invalid transaction fixture"+    , review "sparse-write-value-budget" (sparseRows BINLOG_WRITE_ROWS_EVENTv1)+    , review "sparse-delete-value-budget" (sparseRows BINLOG_DELETE_ROWS_EVENTv1)+    , review "sparse-update-value-budget" (sparseRows BINLOG_UPDATE_ROWS_EVENTv1)+    , review "row-count-budget-preserved" $ expectRejection $ decode defaultDecodeLimits+        [tableMap 1 [3], packet BINLOG_WRITE_ROWS_EVENTv1 $ do+            putWord48le 1 >> putWord16le 0 >> putLenEncInt 1 >> putWord8 1+            replicateM_ 65537 (putWord8 1)]+    , review "decoded-value-budget-preserved" $ expectRejection $ decode defaultDecodeLimits+        [tableMap 1 (replicate 64 3),packet BINLOG_WRITE_ROWS_EVENTv1 $ do+            putWord48le 1 >> putWord16le 0 >> putLenEncInt 64 >> putWord64le maxBound+            replicateM_ 16385 (putWord64le maxBound)]+    , review "event-limit-max-bound" $ do+        (conn,_,_) <- ackFixture (transaction 1 7)+        stream <- openCDC conn 610126 (defaultCDCOptions (FromBinLog (BinLogTracker "bin.000001" 4)))+            {cdcAcknowledgements=AcknowledgeManually,+             cdcDecodeLimits=defaultDecodeLimits {maxExpandedEventBytes=maxBound}}+        let untilCheckpoint = do+                event <- S.read (cdcEvents stream) >>= maybe (fail "EOF before checkpoint") pure+                maybe untilCheckpoint (const (pure ())) (cdcEventCheckpoint event)+        untilCheckpoint `finally` cdcClose stream+    , review "wire-limit-stops-continuations" $ do+        (original,_,closed) <- ackFixture (transaction 1 7)+        count <- newIORef (0::Int)+        let originalInput=C.mysqlRead original+        input <- S.makeInputStream $ do+            value <- S.read originalInput+            modifyIORef' count (+1)+            readCount <- readIORef count+            -- Discovery has 6 packets, SET has one, then ROTATE and FDE.+            -- The first oversized physical fragment must fail without asking+            -- for another continuation. Its body is irrelevant to this check.+            pure (if readCount == 10 then Just (Packet 16777215 1 "") else value)+        let conn=original {C.mysqlRead=input}+        stream <- openCDC conn 610126 (defaultCDCOptions (FromBinLog (BinLogTracker "bin.000001" 4)))+            {cdcAcknowledgements=AcknowledgeManually,+             cdcDecodeLimits=defaultDecodeLimits {maxExpandedEventBytes=1024}}+        expectRejection (S.read (cdcEvents stream))+        equal "oversized fragment stopped before another read" 10 =<< readIORef count+        equal "oversized stream retired" Broken =<< connectionStatus conn+        equal "transport released once" 1 =<< closed+    , review "live-temporal-values" liveTemporal+    , review "live-origin-change-rotation" liveOrigins+    , review "live-two-phase-alter-rejected" liveTwoPhaseAlter+    ]++sparseRows :: BinLogEventType -> IO ()+sparseRows typ = do+    let update=typ == BINLOG_UPDATE_ROWS_EVENTv1+        image=packet typ $ do+            putWord48le 1 >> putWord16le 0 >> putLenEncInt 4096+            putWord8 1 >> putByteString (B.replicate 511 0)+            when update (putByteString (B.replicate 511 0) >> putWord8 128)+            replicateM_ 1000 $ do+                putWord8 0 >> putWord32le 7+                when update (putWord8 0 >> putWord32le 8)+    events <- decode defaultDecodeLimits [tableMap 1 (replicate 4096 3),image]+    case map cdcEventPayload events of+        [CDCWrite _ value] -> equal "all sparse inserts" (replicate 1000 [BinLogLong 7]) (writeRowData value)+        [CDCDelete _ value] -> equal "all sparse deletes" (replicate 1000 [BinLogLong 7]) (deleteRowData value)+        [CDCUpdate _ value] -> equal "all sparse updates" (replicate 1000 ([BinLogLong 7],[BinLogLong 8])) (updateRowData value)+        _ -> fail "missing sparse images"++-- A real rotation writes the full binlog history, including an older origin in+-- the same domain. That list is not a one-entry-per-domain connect state.+liveOrigins :: IO ()+liveOrigins = withIsolated "stage5.review-live-origin-change-rotation" $ \open -> withConnect open $ \writer -> do+    exec writer "SET GLOBAL rpl_semi_sync_master_enabled=0"+    start <- sourceGtids writer+    exec writer "INSERT INTO changes VALUES(1)"+    exec writer "SET SESSION server_id=610131"+    exec writer "INSERT INTO changes VALUES(2)"+    exec writer "FLUSH LOGS"+    exec writer "INSERT INTO changes VALUES(3)"+    withConnect open $ \source -> do+        stream <- openCDC source 610132 (defaultCDCOptions (FromGtid start)) {cdcNonBlocking=True}+        events <- S.toList (cdcEvents stream)+        equal "all origin changes delivered" [[BinLogLong 1],[BinLogLong 2],[BinLogLong 3]]+            (concat [writeRowData row | event <- events,CDCWrite _ row <- [cdcEventPayload event]])+        let histories=[gtidListEntries value | event <- events,CDCGtidList value <- [cdcEventPayload event]]+        assert (any (\entries -> any ((==610129) . gtidServerId) entries+                    && any ((==610131) . gtidServerId) entries) histories)+            "rotation did not retain both origin histories"++liveTemporal :: IO ()+liveTemporal = withIsolated "stage5.review-live-temporal-values" $ \open -> withConnect open $ \writer -> do+    exec writer "SET GLOBAL rpl_semi_sync_master_enabled=0"+    let columns = concat [["t" ++ show p ++ " TIME(" ++ show p ++ ")",+                          "d" ++ show p ++ " DATETIME(" ++ show p ++ ")",+                          "s" ++ show p ++ " TIMESTAMP(" ++ show p ++ ")"] | (p,_,_) <- fractions]+    exec writer ("CREATE TABLE temporal_values(id INT," ++ intercalate "," columns ++ ")")+    seconds <- fromInteger <$> scalarInt writer "SELECT UNIX_TIMESTAMP('2026-01-02 03:04:05')"+    start <- sourceGtids writer+    forM_ [(1,False,False),(2,True,False),(3,True,True)] $ \(ident,negative,whole) -> do+        let quote text="'" ++ text ++ "'"+            suffix p | p == 0 = ""+                     | otherwise = "." ++ if whole then replicate (fromIntegral p) '0' else take (fromIntegral p) "123456"+            values = concat [[quote ((if negative then "-" else "") ++ "01:02:03" ++ suffix p),+                              quote ("2026-01-02 03:04:05" ++ suffix p),+                              quote ("2026-01-02 03:04:05" ++ suffix p)] | (p,_,_) <- fractions]+        exec writer ("INSERT INTO temporal_values VALUES(" ++ intercalate "," (show (ident::Int):values) ++ ")")+    withConnect open $ \source -> do+        stream <- openCDC source 610132 (defaultCDCOptions (FromGtid start)) {cdcNonBlocking=True}+        events <- S.toList (cdcEvents stream)+        let expected = [BinLogLong ident : concat+                [[BinLogTime2 (if negative then 0 else 1) 1 2 3 micro,+                  BinLogDateTime2 2026 1 2 3 4 5 micro,BinLogTimeStamp2 seconds micro]+                | (_,_,fraction) <- fractions,let micro=if whole then 0 else fraction]+                | (ident,negative,whole) <- [(1,False,False),(2,True,False),(3,True,True)]]+        equal "real temporal rows at every precision" expected+            (concat [writeRowData row | event <- events,CDCWrite _ row <- [cdcEventPayload event]])++liveTwoPhaseAlter :: IO ()+liveTwoPhaseAlter = withIsolated "stage5.review-live-two-phase-alter-rejected" $ \open -> withConnect open $ \writer -> do+    exec writer "SET GLOBAL rpl_semi_sync_master_enabled=0"+    start <- sourceGtids writer+    exec writer "SET SESSION binlog_alter_two_phase=ON"+    exec writer "ALTER TABLE changes ADD n INT, ALGORITHM=COPY"+    withConnect open $ \source -> do+        stream <- openCDC source 610132 (defaultCDCOptions (FromGtid start)) {cdcNonBlocking=True}+        expectUnsupported (S.toList (cdcEvents stream))+        equal "unsupported DDL retires source" Broken =<< connectionStatus source
+ validation/hs/Validation/Stage6.hs view
@@ -0,0 +1,625 @@+{-# LANGUAGE ScopedTypeVariables #-}+-- | Literal protocol fixtures and real MariaDB checks for streamed parameters,+-- cursors, progress and compression. Inventory checks call real public APIs.+module Validation.Stage6 (cases, longDataFeature, cursorFeature, progressFeature, compressionFeature) where++import Control.Concurrent+import Control.Exception hiding (assert)+import Control.Monad+import Data.Binary.Put+import qualified Data.Binary as Binary+import qualified Codec.Compression.Zlib as Zlib+import Data.Bits+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as L+import Data.IORef+import qualified Data.Text as T+import Data.Word+import Database.MySQL.Base+import qualified Database.MySQL.Connection as C+import qualified Database.MySQL.TLS as TLS+import qualified System.IO.Streams as S+import System.Environment (getEnv)+import System.Timeout+import Validation.Common+import Validation.FaultTransport (scripted)+import Validation.WireProxy (withWireProxy)++field :: ColumnDef+field = ColumnDef "" "" "" "n" "" 63 20 mySQLTypeLongLong 0 0++preparePackets :: Int -> Int -> [Packet]+preparePackets parameters columns =+    putToPacket 1 (putWord8 0 >> putWord32le 9 >> putWord16le (fromIntegral columns) >>+        putWord16le (fromIntegral parameters) >> putWord8 0 >> putWord16le 0) :+    [encodeToPacket (fromIntegral n) field | n <- [2..1+parameters+columns]]++ending :: Word8 -> Word16 -> Packet+ending n flags = putToPacket n (putWord8 254 >> putWord8 0 >> putWord8 0 >> putWord16le flags >> putWord16le 0)++binaryRow :: Word8 -> Word64 -> Packet+binaryRow n value = putToPacket n (putWord8 0 >> putWord8 0 >> putWord64le value)++statementFixture :: ConnectionOptions -> Int -> Int -> [Packet]+                 -> IO (MySQLConn, PreparedStatement, IO [Packet], IO Int)+statementFixture options parameters columns packets = do+    (conn,sent,closed) <- scripted options (preparePackets parameters columns ++ packets)+    writeIORef (C.connectionCapabilities conn) (bit 24)+    statement <- prepareStatement conn "SELECT ?"+    pure (conn,statement,sent,closed)++cursorFixture :: [Packet] -> IO (MySQLConn, PreparedCursor, IO [Packet], IO Int)+cursorFixture batches = do+    (conn,statement,sent,closed) <- statementFixture defaultConnectionOptions 0 1+        ([putToPacket 1 (putWord8 1),encodeToPacket 2 field,ending 3 66] ++ batches)+    (_,cursor) <- openCursor statement []+    pure (conn,cursor,sent,closed)++expectStatementError :: (StatementException -> Bool) -> IO a -> IO ()+expectStatementError matches action = do+    result <- try action+    case result of+        Left err -> assert (matches err) ("wrong statement exception: " ++ show err)+        Right _ -> fail "expected a local statement error"++expectLocal :: (ConnectionError -> Bool) -> IO a -> IO ()+expectLocal matches action = do+    result <- try action+    case result of+        Left err -> assert (matches err) ("wrong connection exception: " ++ show err)+        Right _ -> fail "expected a local connection error"++assertRetired :: MySQLConn -> IO Int -> IO a -> IO ()+assertRetired conn closed action = do+    expectRejection action+    equal "retired" Broken =<< connectionStatus conn+    equal "released once" 1 =<< closed++progressPacket :: Word8 -> B.ByteString -> Packet+progressPacket n info = putToPacket n $ do+    putByteString (B.pack [255,255,255,1,1,2,80,195,0]) -- 50.000%, stage 1/2.+    putLenEncBytes info++-- Independent compressed-frame headers, including raw-frame representation.+frame :: Word8 -> Int -> L.ByteString -> L.ByteString+frame n expanded payload = runPut $ do+    put24 (fromIntegral (L.length payload)); putWord8 n; put24 expanded; putLazyByteString payload+  where+    put24 :: Int -> Put+    put24 x = mapM_ (putWord8 . fromIntegral . shiftR x) [0,8,16]++compressedFixture :: [B.ByteString] -> ConnectionOptions+                  -> IO (MySQLConn, IO [L.ByteString], IO Int)+compressedFixture chunks options = do+    input <- S.fromList chunks+    sent <- newIORef []+    closed <- newIORef (0::Int)+    conn <- C.newTransportConnection options {enableCompression=True} input+        (\bytes -> modifyIORef' sent (bytes:)) (modifyIORef' closed (+1))+    writeIORef (C.connectionCapabilities conn) (bit 5)+    C.connectionCompressionStart conn+    pure (conn,reverse <$> readIORef sent,readIORef closed)++case6 :: String -> IO () -> Case+case6 name = Case ("stage6." ++ name) "stage6"++withOptions :: ConnectionOptions -> (MySQLConn -> IO a) -> IO a+withOptions options action = config >>= \ci -> withConnect (connectWithOptions options ci) action++longDataFeature :: IO ()+longDataFeature = withConn $ \conn -> do+    exec conn "CREATE TEMPORARY TABLE mb_stage6_long(n INT,b LONGBLOB,t LONGTEXT)"+    withPreparedStatement conn "INSERT INTO mb_stage6_long VALUES(?,?,?)" $ \statement -> do+        bytes <- S.fromList [B.pack [0..255],B.replicate 200000 97]+        text <- S.fromList ["雪",T.replicate 10000 "😀é"]+        ok <- executePreparedStream statement [PreparedValue (MySQLInt32 7),PreparedBytes bytes,PreparedText text]+        equal "inserted" 1 (okAffectedRows ok)+    equal "all bytes/text stored" [[7,200256,60003,20001]] . map (map number) =<<+        rows conn "SELECT n,OCTET_LENGTH(b),OCTET_LENGTH(t),CHAR_LENGTH(t) FROM mb_stage6_long"+    healthy conn++cursorFeature :: IO ()+cursorFeature = withConn $ \conn -> withPreparedStatement conn "SELECT seq FROM seq_1_to_23" $ \statement -> do+    withCursor statement [] $ \cursor -> do+        let loop acc = do+                done <- cursorExhausted cursor+                if done then pure acc else do+                    stream <- fetchCursor cursor 5+                    values <- S.toList stream+                    decoded <- forM values $ \row -> case row of+                        [value] -> pure (number value)+                        _ -> fail "unexpected cursor row width"+                    loop (acc ++ decoded)+        equal "cursor order and final batch" [1..23] =<< loop []+    healthy conn++progressFeature :: IO ()+progressFeature = withOptions defaultConnectionOptions {enableProgressReporting=True} $ \conn -> do+    reports <- newIORef []+    setProgressCallback conn (Just (\p -> modifyIORef' reports (p:)))+    exec conn "CREATE TEMPORARY TABLE mb_stage6_progress(n INT,v VARCHAR(200)) ENGINE=Aria"+    exec conn "INSERT INTO mb_stage6_progress SELECT seq,REPEAT('x',200) FROM seq_1_to_20000"+    exec conn "ALTER TABLE mb_stage6_progress ADD INDEX(n),ALGORITHM=COPY"+    actual <- readIORef reports+    assert (not (null actual)) "real ALTER must deliver at least one progress report"+    assert (all (\p -> progressStage p > 0 && progressStage p <= progressStageCount p && progressThousandths p <= 100000) actual)+        "valid real progress stages"+    healthy conn++compressionFeature :: IO ()+compressionFeature = withOptions defaultConnectionOptions {enableCompression=True} $ \conn -> do+    equal "compressed value" (T.replicate 100000 "é雪") . textOf =<< scalar conn "SELECT REPEAT('é雪',100000)"+    withPreparedStatement conn "SELECT OCTET_LENGTH(?),?" $ \statement -> do+        (_,stream) <- queryPrepared statement [MySQLBytes (B.replicate 100000 65),MySQLInt64 7]+        equal "compressed prepared parity" [[100000,7]] . map (map number) =<< S.toList stream+    healthy conn++cases :: [Case]+cases =+    [ case6 "long-data-live" longDataFeature+    , case6 "cursor-live" cursorFeature+    , case6 "progress-live" progressFeature+    , case6 "compression-live" compressionFeature+    , case6 "long-data-query-empty-and-binary" $ withConn $ \conn ->+        withPreparedStatement conn "SELECT OCTET_LENGTH(?),HEX(?)" $ \statement -> do+            empty <- S.fromList []+            bytes <- S.fromList [B.pack [0,255,0],B.pack [128,1]]+            (_,stream) <- queryPreparedStream statement [PreparedBytes empty,PreparedBytes bytes]+            values <- S.toList stream+            case values of+                [[size,value]] -> do+                    equal "empty non-NULL" 0 (number size)+                    equal "exact bytes" "00FF008001" (textOf value)+                _ -> fail "unexpected streamed query result shape"+            healthy conn+    , case6 "cursor-close-pending-batch" $ withConn $ \conn ->+        withPreparedStatement conn "SELECT seq FROM seq_1_to_1000" $ \statement -> do+            (_,cursor) <- openCursor statement []+            batch <- fetchCursor cursor 100+            equal "first cursor row" (Just [1]) . fmap (map number) =<< S.read batch+            closeCursor cursor+            closeCursor cursor+            healthy conn+            (_,stream) <- queryPrepared statement []+            equal "statement survives reset" 1000 . length =<< S.toList stream+    , case6 "compression-tls" $ do+        ci <- config+        ca <- getEnv "MB_TLS_CA"+        params <- TLS.makeClientParams (TLS.CustomCAStore ca)+        withConnect (TLS.connectWithOptions defaultConnectionOptions {enableCompression=True} ci (params,"localhost")) $ \conn -> do+            equal "TLS compressed length" 1000000 =<< scalarInt conn "SELECT OCTET_LENGTH(REPEAT('x',1000000))"+            healthy conn+    , case6 "compression-socket" $ do+        ci <- config; path <- socketPath+        withConnect (connectUnixSocketWithOptions defaultConnectionOptions {enableCompression=True} path ci) healthy+    ] ++ longDataCases ++ cursorCases ++ progressCases ++ compressionCases ++ lifecycleCases++longDataCases :: [Case]+longDataCases =+    [ case6 "long-data-wire-omission" $ do+        (_,statement,sent,_) <- statementFixture defaultConnectionOptions 1 0 [encodeToPacket 1 (OK 1 0 2 0)]+        source <- S.fromList ["ab","cd"]+        void (executePreparedStream statement [PreparedBytes source])+        packets <- sent+        equal "prepare, empty marker, coalesced chunk, execute" [22,24,24,23] (map (L.head . pBody) packets)+        equal "coalesced data retains byte order" "abcd" (L.drop 7 (pBody (packets !! 2)))+        equal "uploaded parameter has type but no inline value"+            (L.pack [23,9,0,0,0,0,1,0,0,0,0,1,252,0]) (pBody (last packets))+        equal "each no-reply command resets sequence" [0,0,0,0] (map pSeqN packets)+    , case6 "long-data-arity-does-not-read-source" $ do+        (conn,statement,sent,_) <- statementFixture defaultConnectionOptions 1 0 []+        consumed <- newIORef (0::Int)+        source <- S.makeInputStream (modifyIORef' consumed (+1) >> pure Nothing)+        result <- try (executePreparedStream statement [PreparedBytes source,PreparedValue MySQLNull])+        case result of Left WrongParamsCount -> pure (); _ -> fail "expected exact arity rejection"+        equal "no source reads" 0 =<< readIORef consumed+        equal "no upload writes" 1 . length =<< sent+        equal "connection still ready" Ready =<< connectionStatus conn+    , case6 "long-data-budget" $ do+        (conn,statement,_,closed) <- statementFixture defaultConnectionOptions {maximumLongDataBytes=3} 1 0 []+        source <- S.fromList ["ab","cd"]+        assertRetired conn closed (executePreparedStream statement [PreparedBytes source])+    , case6 "long-data-source-failure" $ do+        (conn,statement,_,closed) <- statementFixture defaultConnectionOptions 1 0 []+        source <- S.makeInputStream (throwIO (userError "source-marker"))+        result <- try (executePreparedStream statement [PreparedBytes source])+        case result of+            Left (err::SomeException) -> assert ("source-marker" `T.isInfixOf` T.pack (displayException err)) "source error identity"+            Right _ -> fail "source did not fail"+        equal "retired after no-reply command" Broken =<< connectionStatus conn+        equal "one close" 1 =<< closed+    , case6 "long-data-cancellation" $ do+        (conn,statement,_,closed) <- statementFixture defaultConnectionOptions 1 0 []+        blocked <- newEmptyMVar+        source <- S.makeInputStream (takeMVar blocked :: IO (Maybe B.ByteString))+        result <- timeout 20000 (executePreparedStream statement [PreparedBytes source])+        equal "cancel propagated" Nothing result+        equal "cancel retired" Broken =<< connectionStatus conn+        equal "one release" 1 =<< closed+    , case6 "long-data-exclusive-source" $ do+        (conn,statement,sent,_) <- statementFixture defaultConnectionOptions 1 0 [encodeToPacket 1 (OK 1 0 2 0)]+        source <- S.makeInputStream $ do+            expectLocal (\e -> case e of ConcurrentConnectionUse -> True; _ -> False) (ping conn)+            pure Nothing+        void (executePreparedStream statement [PreparedBytes source])+        equal "no nested ping bytes" [22,24,23] . map (L.head . pBody) =<< sent+    , case6 "long-data-large-chunk-split" $ do+        (_,statement,sent,_) <- statementFixture defaultConnectionOptions 1 0 [encodeToPacket 1 (OK 1 0 2 0)]+        source <- S.fromList [B.replicate 200000 42]+        void (executePreparedStream statement [PreparedBytes source])+        packets <- sent+        let sizes = [pLen p-7 | p <- packets,L.head (pBody p)==24]+        equal "bounded chunk lengths" [0,65536,65536,65536,3392] sizes+    , case6 "long-data-following-normal-execution" $ withConn $ \conn ->+        withPreparedStatement conn "SELECT OCTET_LENGTH(?)" $ \statement -> do+            forM_ [100,0,200] $ \size -> do+                source <- S.fromList [B.replicate size 42]+                (_,stream) <- queryPreparedStream statement [PreparedBytes source]+                equal "new upload replaces prior state" [[toInteger size]] . map (map number) =<< S.toList stream+                (_,normal) <- queryPrepared statement [MySQLBytes "abc"]+                equal "normal execute has no old upload" [[3]] . map (map number) =<< S.toList normal+    , case6 "long-data-text-client-charset" $ withConn $ \conn -> do+        exec conn "SET NAMES latin1"+        withPreparedStatement conn "SELECT HEX(?)" $ \statement -> do+            source <- S.fromList ["é"]+            (_,stream) <- queryPreparedStream statement [PreparedText source]+            values <- S.toList stream+            equal "latin1 streamed bytes" [["E9"]] (map (map textOf) values)+    , case6 "long-data-rollback" $ withConn $ \conn -> do+        exec conn "CREATE TEMPORARY TABLE mb_stage6_rollback(v BLOB) ENGINE=InnoDB"+        withPreparedStatement conn "INSERT INTO mb_stage6_rollback VALUES(?)" $ \statement -> do+            result <- try $ withTransaction conn $ do+                source <- S.fromList ["abc"]+                void (executePreparedStream statement [PreparedBytes source])+                throwIO (userError "rollback-marker")+            case result of Left (_::SomeException) -> pure (); Right () -> fail "expected body exception"+        equal "streamed insert rolled back" 0 =<< scalarInt conn "SELECT COUNT(*) FROM mb_stage6_rollback"+    ]++cursorCases :: [Case]+cursorCases =+    [ case6 "cursor-wire-and-final-empty-fetch" $ do+        (_,cursor,sent,_) <- cursorFixture [binaryRow 1 7,ending 2 66,ending 1 130]+        first <- fetchCursor cursor 1 >>= S.toList+        equal "binary row" [[7]] (map (map number) first)+        equal "server still reports open" False =<< cursorExhausted cursor+        equal "final empty batch" [] =<< (fetchCursor cursor 1 >>= S.toList)+        equal "server exhausted" True =<< cursorExhausted cursor+        packets <- sent+        equal "read-only execute flag" 1 (L.index (pBody (packets !! 1)) 5)+        equal "literal FETCH" (L.pack [28,9,0,0,0,1,0,0,0]) (pBody (last packets))+    , case6 "cursor-invalid-count-is-local" $ do+        (conn,cursor,sent,_) <- cursorFixture [encodeToPacket 1 (OK 0 0 2 0)]+        forM_ [0,65537] $ \n -> expectStatementError (\e -> case e of InvalidCursorUse _ -> True; _ -> False) (fetchCursor cursor n)+        equal "no FETCH sent" 2 . length =<< sent+        equal "cursor still owns connection" Streaming =<< connectionStatus conn+        closeCursor cursor+    , case6 "cursor-unfinished-batch-is-local" $ do+        (_,cursor,sent,_) <- cursorFixture [binaryRow 1 7,ending 2 66,encodeToPacket 1 (OK 0 0 2 0)]+        batch <- fetchCursor cursor 2+        expectStatementError (\e -> case e of InvalidCursorUse _ -> True; _ -> False) (fetchCursor cursor 2)+        equal "one fetch only" 3 . length =<< sent+        equal "batch remains readable" [[7]] . map (map number) =<< S.toList batch+        closeCursor cursor+    , case6 "cursor-more-rows-than-requested" $ do+        (conn,cursor,_,closed) <- cursorFixture [binaryRow 1 7,binaryRow 2 8,ending 3 130]+        assertRetired conn closed (fetchCursor cursor 1 >>= S.toList)+    , case6 "cursor-invalid-end-status" $ do+        (conn,cursor,_,closed) <- cursorFixture [ending 1 2]+        assertRetired conn closed (fetchCursor cursor 2 >>= S.toList)+    , case6 "cursor-close-without-fetch" $ do+        (conn,cursor,sent,_) <- cursorFixture [encodeToPacket 1 (OK 0 0 2 0)]+        closeCursor cursor+        equal "close uses RESET without fetching" [22,23,26] . map (L.head . pBody) =<< sent+        equal "ready after cursor close" Ready =<< connectionStatus conn+    , case6 "cursor-foreign-thread" $ do+        (_,cursor,sent,_) <- cursorFixture [encodeToPacket 1 (OK 0 0 2 0)]+        done <- newEmptyMVar+        _ <- forkIO (try (fetchCursor cursor 1) >>= putMVar done)+        result <- takeMVar done+        case result of Left ConcurrentConnectionUse -> pure (); _ -> fail "foreign thread must be rejected"+        equal "no foreign writes" 2 . length =<< sent+        closeCursor cursor+    , case6 "cursor-normal-query-blocked" $ withConn $ \conn ->+        withPreparedStatement conn "SELECT seq FROM seq_1_to_100" $ \statement ->+            withCursor statement [] $ \cursor -> do+                result <- try (ping conn)+                case result of Left UnconsumedResultSet -> pure (); _ -> fail "cursor must retain command lease"+                batch <- fetchCursor cursor 3+                void (S.read batch)+    , case6 "cursor-cleanup-transaction-rollback" $ withConn $ \conn -> do+        exec conn "CREATE TEMPORARY TABLE mb_stage6_cursor_tx(n INT) ENGINE=InnoDB"+        void (try (withTransaction conn $ do+            exec conn "INSERT INTO mb_stage6_cursor_tx VALUES(1)"+            withPreparedStatement conn "SELECT seq FROM seq_1_to_1000" $ \statement ->+                withCursor statement [] $ \cursor -> do+                    batch <- fetchCursor cursor 50+                    void (S.read batch)+                    throwIO (userError "cursor-body-marker")) :: IO (Either SomeException ()))+        equal "rollback after cursor release" 0 =<< scalarInt conn "SELECT COUNT(*) FROM mb_stage6_cursor_tx"+        healthy conn+    , case6 "cursor-empty-live" $ withConn $ \conn ->+        withPreparedStatement conn "SELECT 1 WHERE 0" $ \statement ->+            withCursor statement [] $ \cursor -> do+                equal "empty cursor" [] =<< (fetchCursor cursor 10 >>= S.toList)+                equal "empty exhaustion" True =<< cursorExhausted cursor+    , case6 "cursor-metadata-cache-reexecution" $ withConn $ \conn ->+        withPreparedStatement conn "SELECT CAST(? AS SIGNED)" $ \statement -> do+            forM_ [1..3] $ \n -> withCursor statement [MySQLInt64 n] $ \cursor -> do+                equal "cached cursor metadata" [[toInteger n]] . map (map number) =<< (fetchCursor cursor 2 >>= S.toList)+            (_,normal) <- queryPrepared statement [MySQLInt64 9]+            equal "normal after cursor" [[9]] . map (map number) =<< S.toList normal+    ]++progressCases :: [Case]+progressCases =+    [ case6 "progress-literal-envelope" $ do+        (conn,_,_) <- scripted defaultConnectionOptions {enableProgressReporting=True}+            [progressPacket 1 "building",encodeToPacket 2 (OK 0 0 2 0)]+        writeIORef (C.connectionCapabilities conn) (bit 32)+        reports <- newIORef []+        setProgressCallback conn (Just (\p -> modifyIORef' reports (p:)))+        void (ping conn)+        equal "literal progress decoded" [Progress 1 2 50000 "building"] =<< readIORef reports+    , case6 "progress-callback-failure-retires" $ do+        (conn,_,closed) <- scripted defaultConnectionOptions {enableProgressReporting=True}+            [progressPacket 1 "building",encodeToPacket 2 (OK 0 0 2 0)]+        writeIORef (C.connectionCapabilities conn) (bit 32)+        setProgressCallback conn (Just (const (throwIO (userError "callback-marker"))))+        assertRetired conn closed (ping conn)+    , case6 "progress-callback-reentry-rejected" $ do+        (conn,sent,_) <- scripted defaultConnectionOptions {enableProgressReporting=True}+            [progressPacket 1 "",encodeToPacket 2 (OK 0 0 2 0)]+        writeIORef (C.connectionCapabilities conn) (bit 32)+        setProgressCallback conn (Just (const (expectLocal (\e -> case e of ConcurrentConnectionUse -> True; _ -> False) (ping conn))))+        void (ping conn)+        equal "only original ping" 1 . length =<< sent+    , case6 "progress-discard-without-handler" $ do+        (conn,_,_) <- scripted defaultConnectionOptions {enableProgressReporting=True}+            [progressPacket 1 "",progressPacket 2 "next",encodeToPacket 3 (OK 0 0 2 0)]+        writeIORef (C.connectionCapabilities conn) (bit 32)+        void (ping conn)+        equal "reports are not command completion" Ready =<< connectionStatus conn+    , case6 "progress-not-negotiated" $ do+        (conn,sent,_) <- scripted defaultConnectionOptions []+        expectLocal (\e -> case e of ProtocolError _ -> True; _ -> False) (setProgressCallback conn Nothing)+        equal "no negotiation or callback SQL" [] =<< sent+    ] +++    [ case6 ("progress-malformed-" ++ name) $ do+        (conn,_,closed) <- scripted defaultConnectionOptions {enableProgressReporting=True} [Packet (L.length body) 1 body]+        writeIORef (C.connectionCapabilities conn) (bit 32)+        assertRetired conn closed (ping conn)+    | (name,body) <-+        [("count",L.pack [255,255,255,2,1,2,0,0,0,0]),+         ("stage",L.pack [255,255,255,1,0,2,0,0,0,0]),+         ("percent",L.pack [255,255,255,1,1,2,255,255,255,0]),+         ("truncated",L.pack [255,255,255,1,1]),+         ("trailer",pBody (progressPacket 1 "") <> L.singleton 1)]+    ]++compressionCases :: [Case]+compressionCases =+    [ case6 ("compression-byte-split-" ++ show split) $ do+        let bytes = L.toStrict (frame 1 0 (Binary.encode (encodeToPacket 1 (OK 0 0 2 0))))+        (conn,_,_) <- compressedFixture [B.take split bytes,B.drop split bytes] defaultConnectionOptions+        void (ping conn)+        equal "valid split" Ready =<< connectionStatus conn+    | split <- [1..17]+    ] +++    [ case6 "compression-zlib-envelope" $ do+        (conn,_,_) <- compressedFixture [L.toStrict (frame 1 (fromIntegral (L.length ordinary)) (Zlib.compress ordinary))] defaultConnectionOptions+        void (ping conn)+    , case6 "compression-small-write-raw-frame" $ do+        (conn,sent,_) <- compressedFixture [L.toStrict (frame 1 0 (Binary.encode (encodeToPacket 1 (OK 0 0 2 0))))] defaultConnectionOptions+        void (ping conn)+        equal "literal compressed-transport PING" [L.pack [5,0,0,0,0,0,0,1,0,0,0,14]] =<< sent+    , case6 "compression-large-roundtrip" $ withOptions defaultConnectionOptions {enableCompression=True} $ \conn ->+        withPreparedStatement conn "SELECT ?" $ \statement -> do+            let bytes = B.replicate 16777216 90+            (_,stream) <- queryPrepared statement [MySQLBytes bytes]+            equal "continued compressed row" [[MySQLBytes bytes]] =<< S.toList stream+            healthy conn+    , case6 "all-features-together" $ withOptions defaultConnectionOptions {enableCompression=True,enableProgressReporting=True} $ \conn -> do+        setProgressCallback conn (Just (const (pure ())))+        withPreparedStatement conn "SELECT OCTET_LENGTH(?)" $ \statement -> do+            source <- S.fromList (replicate 20 (B.replicate 65536 42))+            (_,stream) <- queryPreparedStream statement [PreparedBytes source]+            equal "compressed streamed parameters" [[1310720]] . map (map number) =<< S.toList stream+        withPreparedStatement conn "SELECT seq FROM seq_1_to_100" $ \statement ->+            withCursor statement [] $ \cursor -> do+                values <- fetchCursor cursor 200 >>= S.toList+                equal "compressed cursor" 5050 (sum (map (sum . map number) values))+        void (resetConnection conn)+        healthy conn+    ] +++    [ case6 ("compression-invalid-" ++ name) $ do+        (conn,_,closed) <- compressedFixture [L.toStrict bytes] defaultConnectionOptions+        assertRetired conn closed (ping conn)+    | (name,bytes) <-+        [("sequence",frame 2 0 ordinary),+         ("zlib",frame 1 11 "garbage"),+         ("length",frame 1 12 (Zlib.compress ordinary)),+         ("expansion",frame 1 1 (Zlib.compress ordinary)),+         ("trailing",frame 1 11 (Zlib.compress ordinary <> "tail")),+         ("empty",frame 1 0 ""),+         ("short-header",L.pack [1,0,0]),+         ("short-body",L.take 10 (frame 1 0 ordinary))]+    ]+  where ordinary = Binary.encode (encodeToPacket 1 (OK 0 0 2 0))++-- These cases exercise the boundaries between the new APIs and existing+-- cancellation, metadata, transaction and session contracts.+lifecycleCases :: [Case]+lifecycleCases =+    [ case6 "cursor-stream-buffered-source-cancellation" $ do+        (conn,statement,_,closed) <- statementFixture defaultConnectionOptions 1 1+            [putToPacket 1 (putWord8 1),encodeToPacket 2 field,ending 3 66,encodeToPacket 1 (OK 0 0 2 0)]+        consumed <- newIORef (0::Int)+        source <- S.makeInputStream $ do+            n <- atomicModifyIORef' consumed (\x -> (x+1,x))+            pure (if n < 10000000 then Just B.empty else Nothing)+        result <- timeout 20000 (withCursorStream statement [PreparedBytes source] (const (pure ())))+        equal "buffered acquisition cancellation propagated" Nothing result+        equal "incomplete upload retired" Broken =<< connectionStatus conn+        equal "acquisition finalizer ran once" 1 =<< closed+        count <- readIORef consumed+        assert (count < 10000000) "source ran to completion under a masked acquisition"+    , case6 "long-data-text-collation-parity" $ withConn $ \conn ->+        withPreparedStatement conn "SELECT COLLATION(?),?='É'" $ \statement ->+            forM_ ["é",""] $ \text -> do+                -- COLLATION is conservatively opaque to the SQL classifier;+                -- explicitly refresh facts before each charset-bound execution.+                refreshSessionState conn+                (_,normal) <- queryPrepared statement [MySQLText text,MySQLText text]+                expected <- S.toList normal+                refreshSessionState conn+                first <- S.fromList [text]+                second <- S.fromList [text]+                (_,streamed) <- queryPreparedStream statement [PreparedText first,PreparedText second]+                equal "text collation and comparison survive upload" expected =<< S.toList streamed+    , case6 "cursor-non-row-more-results-drained" $ do+        (conn,statement,sent,_) <- statementFixture defaultConnectionOptions 0 0+            [encodeToPacket 1 (OK 1 0 10 0),putToPacket 2 (putWord8 1),encodeToPacket 3 field,+             binaryRow 4 7,ending 5 2,encodeToPacket 1 (OK 0 0 2 0)]+        expectStatementError (\e -> case e of InvalidCursorUse _ -> True; _ -> False) (openCursor statement [])+        void (ping conn)+        equal "non-row cursor refusal stays synchronized" [22,23,14] . map (L.head . pBody) =<< sent+    , case6 "cursor-reset-extra-results-rejected" $ do+        (conn,cursor,_,closed) <- cursorFixture [encodeToPacket 1 (OK 0 0 10 0)]+        assertRetired conn closed (closeCursor cursor)+    , case6 "long-data-tiny-chunks-coalesced-with-bounded-segments" $ do+        (_,statement,sent,_) <- statementFixture defaultConnectionOptions 1 0 [encodeToPacket 1 (OK 1 0 2 0)]+        source <- S.fromList (replicate 5000 "x")+        void (executePreparedStream statement [PreparedBytes source])+        packets <- sent+        equal "tiny source segments stay bounded" [0,1024,1024,1024,1024,904]+            [pLen p-7 | p <- packets,L.head (pBody p)==24]+        equal "every tiny byte survives" (L.replicate 5000 120)+            (L.concat [L.drop 7 (pBody p) | p <- packets,L.head (pBody p)==24])+    , case6 "cursor-streamed-parameters" $ withOptions defaultConnectionOptions {enableCompression=True} $ \conn ->+        withPreparedStatement conn "SELECT OCTET_LENGTH(?),seq FROM seq_1_to_3" $ \statement -> do+            source <- S.fromList (replicate 64 (B.replicate 16384 42))+            withCursorStream statement [PreparedBytes source] $ \cursor -> do+                batch <- fetchCursor cursor 5 >>= S.toList+                equal "uploaded parameter used by compressed cursor" [[1048576,1],[1048576,2],[1048576,3]] (map (map number) batch)+            healthy conn+    , case6 "compression-real-wire-byte-savings" $ do+        ci <- config+        counts <- forM [False,True] $ \compressed -> withWireProxy ci $ \proxied counters ->+            withConnect (connectWithOptions defaultConnectionOptions {enableCompression=compressed} proxied) $ \conn -> do+                before <- counters+                equal "complete transfer" (T.replicate 1000000 "x") . textOf =<< scalar conn "SELECT REPEAT('x',1000000)"+                after <- counters+                pure (snd after - snd before)+        case counts of+            [plain,zlib] -> do+                assert (plain > 1000000 && zlib*10 < plain) "real zlib result must reduce wire bytes substantially"+                putStrLn ("STAGE6_WIRE\tplain=" ++ show plain ++ "\tzlib=" ++ show zlib)+            _ -> fail "missing byte-counter alternatives"+    , case6 "cursor-close-after-terminal-connection" $ withConn $ \conn ->+        withPreparedStatement conn "SELECT seq FROM seq_1_to_100" $ \statement -> do+            (_,cursor) <- openCursor statement []+            close conn+            closeCursor cursor+            equal "cursor locally closed" True =<< cursorExhausted cursor+            equal "connection stays closed" Closed =<< connectionStatus conn+    , case6 "long-data-server-error-remains-synchronized" $ do+        let err = putToPacket 1 (putWord8 255 >> putWord16le 1366 >> putByteString "#HY000bad value")+        (conn,statement,sent,closed) <- statementFixture defaultConnectionOptions 1 0+            [err,encodeToPacket 1 (OK 0 0 2 0)]+        source <- S.fromList ["abc"]+        expectERR [1366] (executePreparedStream statement [PreparedBytes source])+        void (ping conn)+        equal "complete server error did not retire" 0 =<< closed+        equal "following ping synchronized" 14 . L.head . pBody . last =<< sent+    , case6 "long-data-closed-handle-no-source-read" $ do+        (_,statement,sent,_) <- statementFixture defaultConnectionOptions 1 0 []+        closePrepared statement+        source <- S.makeInputStream (fail "closed statement consumed source")+        expectStatementError (\e -> case e of StatementClosed -> True; _ -> False)+            (executePreparedStream statement [PreparedBytes source])+        equal "only prepare and close" 2 . length =<< sent+    , case6 "long-data-multiple-results" $ withConn $ \conn ->+        bracket_ (exec conn "CREATE PROCEDURE mb_stage6_long_results(IN b BLOB,OUT n INT) BEGIN SELECT HEX(b) AS b; SET n=OCTET_LENGTH(b); END")+            (exec conn "DROP PROCEDURE mb_stage6_long_results") $+            withPreparedStatement conn "CALL mb_stage6_long_results(?,?)" $ \statement -> do+                source <- S.fromList ["abc"]+                results <- queryPreparedStreamMulti statement [PreparedBytes source,PreparedValue MySQLNull]+                equal "row result with uploaded data" [["616263"]]+                    [map textOf row | PreparedRows _ values <- results,row <- values]+                equal "OUT parameter after upload" [[3]]+                    [map number values | PreparedOutParameters _ values <- results]+    , case6 "cursor-progress-reentry-is-local" $ do+        let options = defaultConnectionOptions {enableProgressReporting=True}+        (conn,statement,sent,_) <- statementFixture options 0 1+            [putToPacket 1 (putWord8 1),encodeToPacket 2 field,ending 3 66,+             progressPacket 1 "fetching",binaryRow 2 7,ending 3 130]+        writeIORef (C.connectionCapabilities conn) (bit 24 .|. bit 32)+        holder <- newIORef Nothing+        setProgressCallback conn (Just (\_ -> do+            cursor <- readIORef holder >>= maybe (fail "cursor not installed") pure+            expectLocal (\e -> case e of ConcurrentConnectionUse -> True; _ -> False) (closeCursor cursor)))+        (_,cursor) <- openCursor statement []+        writeIORef holder (Just cursor)+        equal "callback cannot drain the outer fetch" [[7]] . map (map number) =<< (fetchCursor cursor 3 >>= S.toList)+        equal "no nested RESET" [22,23,28] . map (L.head . pBody) =<< sent+    , case6 "progress-callback-cancellation" $ do+        (conn,_,closed) <- scripted defaultConnectionOptions {enableProgressReporting=True}+            [progressPacket 1 "",encodeToPacket 2 (OK 0 0 2 0)]+        writeIORef (C.connectionCapabilities conn) (bit 32)+        blocked <- newEmptyMVar+        setProgressCallback conn (Just (const (takeMVar blocked)))+        equal "callback cancellation propagates" Nothing =<< timeout 20000 (ping conn)+        equal "callback cancellation retires" Broken =<< connectionStatus conn+        equal "callback release once" 1 =<< closed+    , case6 "compression-bounds-before-body" $ do+        readCount <- newIORef (0::Int)+        header <- newIORef True+        let tooLarge = L.toStrict (L.take 7 (frame 1 1029 "x"))+        input <- S.makeInputStream $ do+            modifyIORef' readCount (+1)+            first <- atomicModifyIORef' header (\x -> (False,x))+            if first then pure (Just tooLarge) else fail "body was read before expansion bound"+        closed <- newIORef (0::Int)+        conn <- C.newTransportConnection defaultConnectionOptions {enableCompression=True,maximumMessageBytes=1024}+            input (const (pure ())) (modifyIORef' closed (+1))+        writeIORef (C.connectionCapabilities conn) (bit 5)+        C.connectionCompressionStart conn+        assertRetired conn (readIORef closed) (ping conn)+        equal "header only" 1 =<< readIORef readCount+    ] +++    [ case6 ("cursor-metadata-parity-" ++ show tracking ++ "-" ++ show caching) $+        withOptions defaultConnectionOptions {enableSessionTracking=tracking,enableMetadataCaching=caching} $ \conn ->+            withPreparedStatement conn "SELECT seq,IF(seq%2,NULL,REPEAT('雪',10)),CAST(seq*1.25 AS DECIMAL(20,2)) FROM seq_1_to_9" $ \statement -> do+                (_,normal) <- queryPrepared statement []+                expected <- S.toList normal+                withCursor statement [] $ \cursor -> do+                    let loop acc = do+                            done <- cursorExhausted cursor+                            if done then pure acc else do+                                batch <- fetchCursor cursor 2 >>= S.toList+                                loop (acc ++ batch)+                    equal "cursor/scalar/NULL/Unicode/decimal parity" expected =<< loop []+    | tracking <- [False,True],caching <- [False,True]+    ] +++    [ case6 ("cursor-" ++ operation ++ "-deadline-retires") $ do+        inputPackets <- newIORef (preparePackets 0 1 +++            [putToPacket 1 (putWord8 1),encodeToPacket 2 field,ending 3 66])+        blocked <- newEmptyMVar+        input <- S.makeInputStream $ do+            packet <- atomicModifyIORef' inputPackets (\xs -> case xs of [] -> ([],Nothing); p:ps -> (ps,Just p))+            maybe (takeMVar blocked) (pure . Just) packet+        closed <- newIORef (0::Int)+        conn <- C.newConnection defaultConnectionOptions {cleanupTimeoutMicros=20000} input+            (const (pure ())) (modifyIORef' closed (+1))+        writeIORef (C.connectionCapabilities conn) (bit 24)+        statement <- prepareStatement conn "SELECT 1"+        (_,cursor) <- openCursor statement []+        if operation == "fetch" then do+            result <- timeout 20000 (fetchCursor cursor 1 >>= S.toList)+            equal "fetch timeout propagated" Nothing result+        else expectLocal (\e -> case e of CleanupDeadlineExceeded -> True; _ -> False) (closeCursor cursor)+        equal "deadline retired" Broken =<< connectionStatus conn+        equal "deadline one release" 1 =<< readIORef closed+    | operation <- ["fetch","close"]+    ]
+ validation/hs/Validation/Stage6Bench.hs view
@@ -0,0 +1,155 @@+-- | Additive Stage 6 workloads. Every alternative consumes and verifies the+-- same values. Two complete warmups precede each measured process. Cursor+-- materialization and upload/execution are timed; connection/preparation are not.+module Validation.Stage6Bench (run, retentionNegativeControl) where++import Control.Exception (evaluate)+import Control.Monad+import qualified Data.ByteString as B+import qualified Data.ByteString.Internal as BI+import Data.List (intercalate, isPrefixOf, isSuffixOf)+import Data.IORef+import qualified Data.Text as T+import Database.MySQL.Base+import GHC.Clock+import GHC.Stats+import System.Mem+import Foreign.Marshal.Utils (fillBytes)+import qualified System.IO.Streams as S+import Validation.Common++run :: String -> Int -> IO ()+run ident iterations+    | iterations <= 0 = fail "positive Stage 6 work count required"+    | ident == "upload.retention" = withConn $ \conn -> do+        exec conn "CREATE TEMPORARY TABLE mb_stage6_upload_heap(v LONGBLOB) ENGINE=InnoDB"+        withPreparedStatement conn "INSERT INTO mb_stage6_upload_heap VALUES(?)" $ \statement -> do+            let operation = do+                    exec conn "TRUNCATE mb_stage6_upload_heap"+                    source <- retentionSource iterations+                    void (executePreparedStream statement [PreparedBytes source])+                    actual <- scalarInt conn "SELECT OCTET_LENGTH(v) FROM mb_stage6_upload_heap"+                    equal "retention upload size" (toInteger iterations) actual+                    equal "retention upload every byte" 1 =<< scalarInt conn+                        ("SELECT BINARY v=BINARY REPEAT('*'," ++ show iterations ++ ") FROM mb_stage6_upload_heap")+                    pure actual+            measure ident (toInteger iterations) (toInteger iterations) operation+    | "upload." `isPrefixOf` ident = withConn $ \conn -> do+        exec conn "CREATE TEMPORARY TABLE mb_stage6_upload_bench(n INT,v LONGBLOB) ENGINE=InnoDB"+        withPreparedStatement conn "INSERT INTO mb_stage6_upload_bench VALUES(?,?)" $ \statement -> do+            let operation = do+                    exec conn "TRUNCATE mb_stage6_upload_bench"+                    -- Generate equivalent data inside timing. The buffered+                    -- alternative must materialize a full MiB; the streamed+                    -- alternative retains only its reusable 16 KiB chunk.+                    withTransaction conn $ forM_ [1..iterations] $ \n -> do+                        -- IO allocation prevents full-laziness from hoisting+                        -- payload construction out of the warmup/measured runs.+                        chunk <- BI.create 16384 (\ptr -> fillBytes ptr (fromIntegral n) 16384)+                        if ident == "upload.streamed" then do+                            source <- S.fromList (replicate 64 chunk)+                            ok <- executePreparedStream statement [PreparedValue (MySQLInt32 (fromIntegral n)),PreparedBytes source]+                            equal "stream insert" 1 (okAffectedRows ok)+                        else if ident == "upload.buffered" then do+                            let payload = B.concat (replicate 64 chunk)+                            ok <- executePrepared statement [MySQLInt32 (fromIntegral n),MySQLBytes payload]+                            equal "buffered insert" 1 (okAffectedRows ok)+                        else fail "unknown upload profile"+                    actual <- scalarInt conn "SELECT SUM(OCTET_LENGTH(v)) FROM mb_stage6_upload_bench"+                    equal "all stored bytes" (toInteger iterations * 1048576) actual+                    equal "every stored byte matches its row" (toInteger iterations) =<<+                        scalarInt conn "SELECT SUM(BINARY v=BINARY REPEAT(CHAR(MOD(n,256)),1048576)) FROM mb_stage6_upload_bench"+                    pure actual+            measure ident (toInteger iterations * 1048576) (toInteger iterations * 1048576) operation+    | "cursor." `isPrefixOf` ident = withConn $ \conn ->+        withPreparedStatement conn "SELECT seq,REPEAT('x',128) FROM seq_1_to_10000" $ \statement -> do+            let operation = if ident == "cursor.stream" then do+                    (_,stream) <- queryPrepared statement []+                    consume stream+                  else withCursor statement [] $ \cursor -> do+                    let batchSize = case ident of+                            "cursor.batch32" -> 32+                            "cursor.batch512" -> 512+                            "cursor.batch4096" -> 4096+                            _ -> 0+                        loop total = do+                            done <- cursorExhausted cursor+                            if done then pure total else do+                                value <- fetchCursor cursor batchSize >>= consume+                                loop (total + value)+                    loop 0+            measure ident (toInteger iterations * 10000) (toInteger iterations * 51285000)+                (sum <$> replicateM iterations operation)+    | "transport." `isPrefixOf` ident = do+        ci <- config+        let compressed = ".zlib" `isSuffixOf` ident+            wide = "transport.wide." `isPrefixOf` ident+            width = if wide then 2048 else 32 :: Int+            statementText = sql ("SELECT seq,REPEAT('x'," ++ show width ++ ") FROM seq_1_to_1000")+            expected = 500500 + 1000 * toInteger width+        withConnect (connectWithOptions defaultConnectionOptions {enableCompression=compressed} ci) $ \conn ->+            withPreparedStatement conn statementText $ \statement -> do+                let operation = queryPrepared statement [] >>= consume . snd+                measure ident (toInteger iterations * 1000) (toInteger iterations * expected)+                    (sum <$> replicateM iterations operation)+    | "progress." `isPrefixOf` ident = do+        ci <- config+        let enabled = ident == "progress.enabled"+        withConnect (connectWithOptions defaultConnectionOptions {enableProgressReporting=enabled} ci) $ \conn -> do+            when enabled (setProgressCallback conn (Just (const (pure ()))))+            let operation = scalarInt conn "SELECT 937"+            measure ident (toInteger iterations) (toInteger iterations * 937)+                (sum <$> replicateM iterations operation)+    | otherwise = fail "unknown Stage 6 benchmark"++-- Each producer read owns fresh payload storage. Repeating slices of one+-- immutable chunk would let a whole-input buffer retain only that one chunk,+-- making the heap checkpoints incapable of detecting the regression.+retentionSource :: Int -> IO (S.InputStream B.ByteString)+retentionSource bytes = do+    produced <- newIORef 0+    S.makeInputStream $ do+        n <- readIORef produced+        if n >= bytes then pure Nothing else do+            let size = min 65536 (bytes-n)+                total = n+size+            chunk <- BI.create size (\ptr -> fillBytes ptr 42 size)+            writeIORef produced total+            when (total `elem` [65536,1048576,16777216,67108864] || total == bytes) $ do+                performGC+                stats <- getRTSStats+                putStrLn ("STAGE6_HEAP\t" ++ show total ++ "\t" ++ show (gcdetails_live_bytes (gc stats)))+            pure (Just chunk)++-- Deliberately wrong consumer, run as a separate correctness process. The same+-- source/checkpoints must expose retaining every chunk before upload starts.+-- Python applies the actual publication guard and requires it to reject this+-- output. No timings from this negative control enter benchmark comparisons.+retentionNegativeControl :: IO ()+retentionNegativeControl = replicateM_ 3 $ do+    source <- retentionSource 67108864+    chunks <- S.toList source+    count <- evaluate (sum (map B.length chunks))+    equal "buffering control consumed the complete source" 67108864 count++consume :: S.InputStream [MySQLValue] -> IO Integer+consume = S.fold (\total row -> total + sum (map value row)) 0+  where+    value (MySQLText text) = toInteger (T.length text)+    value (MySQLBytes bytes) = toInteger (B.length bytes)+    value other = number other++measure :: String -> Integer -> Integer -> IO Integer -> IO ()+measure ident units expected operation = do+    replicateM_ 2 (operation >>= equal "Stage 6 warmup checksum" expected)+    performGC+    before <- getRTSStats+    start <- getMonotonicTimeNSec+    checksum <- operation >>= evaluate+    end <- getMonotonicTimeNSec+    performGC+    after <- getRTSStats+    equal "Stage 6 measured checksum" expected checksum+    putStrLn (intercalate "\t" ["STAGE6",ident,show units,show (end-start),+        show (allocated_bytes after-allocated_bytes before),show (cpu_ns after-cpu_ns before),+        show (gc_cpu_ns after-gc_cpu_ns before),show checksum])
+ validation/hs/Validation/Stage6Review.hs view
@@ -0,0 +1,177 @@+{-# LANGUAGE ScopedTypeVariables #-}+-- | Regressions from the independent Stage 6 review. Cancellation tests keep+-- statements alive beyond their cursors; CDC tests use disposable semisync+-- servers and independently inspect their acknowledged-transaction counters.+module Validation.Stage6Review (cases) where++import Control.Concurrent+import Control.Exception hiding (assert)+import Control.Monad+import Data.Binary.Put+import qualified Data.Binary as Binary+import Data.Bits+import qualified Data.ByteString.Lazy as L+import Data.IORef+import Data.Maybe (isJust)+import Database.MySQL.Base+import Database.MySQL.BinLog+import qualified Database.MySQL.Connection as C+import qualified System.IO.Streams as S+import System.Environment (getExecutablePath, lookupEnv)+import System.Process (callProcess)+import System.Timeout+import Validation.Common+import Validation.FaultTransport (scripted)++cases :: [Case]+cases =+    [ review ("cursor-repeated-cancellation-" ++ name) (repeatedCancellation streamed)+    | (name,streamed) <- [("scalar",False),("streamed",True)]+    ] +++    [ review ("compressed-cdc-" ++ name) (liveAcknowledgements name mode resume)+    | (name,mode,resume) <- [("manual",AcknowledgeManually,False),+                            ("on-read",AcknowledgeOnRead,False),+                            ("resume",AcknowledgeManually,True)]+    ] +++    [ review "compressed-ack-preserves-buffered-reply" bufferedReply+    , review "heap-buffering-negative-control" $ do+        binary <- getExecutablePath+        callProcess "python3" ["validation/stage6_benchmarks.py","--verify-heap-control",binary]+    ]+  where review name = Case ("stage6.review-" ++ name) "stage6"++repeatedCancellation :: Bool -> IO ()+repeatedCancellation streamed = replicateM_ 20 $ do+    let field = ColumnDef "" "" "" "n" "" 63 20 mySQLTypeLongLong 0 0+        parameters = if streamed then 1 else 0 :: Int+        ending = putToPacket 3 (putWord8 254 >> putWord8 0 >> putWord8 0 >> putWord16le 66 >> putWord16le 0)+        prepare = putToPacket 1 (putWord8 0 >> putWord32le 9 >> putWord16le 1 >>+            putWord16le (fromIntegral parameters) >> putWord8 0 >> putWord16le 0)+    (conn,_,closed) <- scripted defaultConnectionOptions+        ([prepare] ++ [encodeToPacket (fromIntegral n) field | n <- [2..2+parameters]] +++         [putToPacket 1 (putWord8 1),encodeToPacket 2 field,ending,encodeToPacket 1 (OK 0 0 2 0)])+    writeIORef (C.connectionCapabilities conn) (bit 24)+    statement <- prepareStatement conn (if streamed then "SELECT ?" else "SELECT 1")+    source <- S.fromList ["abc"]+    started <- newEmptyMVar+    blocked <- newEmptyMVar+    done <- newEmptyMVar+    let body _ = putMVar started () >> takeMVar blocked+        action = if streamed then withCursorStream statement [PreparedBytes source] body else withCursor statement [] body+    bracket (forkFinally action (putMVar done)) killThread $ \worker -> do+        takeMVar started+        -- The second cancellation is pending as the body exception enters+        -- cleanup. It must propagate after protected transport retirement.+        throwTo worker ThreadKilled+        throwTo worker ThreadKilled+        outcome <- timeout 1000000 (takeMVar done) >>= maybe (fail "cursor worker did not finish") pure+        case outcome of+            Left err -> assert (isJust (fromException err :: Maybe AsyncException)) "cancellation identity lost"+            Right () -> fail "cursor cancellation disappeared"+        status <- connectionStatus conn+        assert (status `elem` [Ready,Broken]) ("cursor lease survived cancellation: " ++ show status)+        when (status == Broken) (equal "retirement closes once" 1 =<< closed)+    close conn++-- Independent compressed headers. The first frame intentionally contains two+-- replies; an ACK between reads must not discard the second buffered packet.+bufferedReply :: IO ()+bufferedReply = do+    let raw n = Binary.encode (encodeToPacket n (OK 0 0 2 0))+        frame n payload = runPut $ do+            let size = L.length payload+            mapM_ (putWord8 . fromIntegral . shiftR size) [0,8,16]+            putWord8 n+            mapM_ putWord8 [0,0,0]+            putLazyByteString payload+    input <- S.fromList (map L.toStrict [frame 7 (raw 4 <> raw 5),frame 1 (raw 1)])+    sent <- newIORef []+    conn <- C.newTransportConnection defaultConnectionOptions {enableCompression=True} input+        (\bytes -> modifyIORef' sent (bytes:)) (pure ())+    writeIORef (C.connectionCapabilities conn) (bit 5)+    C.connectionCompressionStart conn+    C.connectionCompressionAllowReplicationResets conn+    C.setExchangeSequence conn 256+    equal "first reply" 4 . pSeqN =<< C.readConnPacket conn+    C.connectionCompressionReset conn+    C.mysqlWrite conn (putToPacket 0 (putWord8 239 >> putWord64le 100 >> putByteString "bin.1"))+    equal "buffered reply survives ACK" 5 . pSeqN =<< C.readConnPacket conn+    equal "server reset accepted only under replication policy" 1 . pSeqN =<< C.readConnPacket conn+    frames <- readIORef sent+    case frames of+        [packet] -> equal "ACK compressed sequence starts at zero" 0 (L.index packet 3)+        _ -> fail "expected exactly one ACK frame"+    close conn++liveAcknowledgements :: String -> AcknowledgementMode -> Bool -> IO ()+liveAcknowledgements name mode resume = do+    socket <- lookupEnv "MB_CDC_SOCKET"+    case socket of+        Nothing -> do+            binary <- getExecutablePath+            callProcess "python3" ["validation/cdc_fixture.py",binary,"case","stage6.review-compressed-cdc-" ++ name]+        Just path -> do+            let info = defaultConnectInfo {ciUser="root",ciPassword="",ciDatabase="cdc_validation"}+                open = connectUnixSocket path info+                compressed = connectUnixSocketWithOptions defaultConnectionOptions {enableCompression=True} path info+                options start = (defaultCDCOptions start) {cdcAcknowledgements=mode}+                consume stream = do+                    rowsSeen <- newIORef (0::Int)+                    let next = do+                            event <- S.read (cdcEvents stream) >>= maybe (fail "EOF before commit") pure+                            case cdcEventPayload event of CDCWrite _ _ -> modifyIORef' rowsSeen (+1); _ -> pure ()+                            maybe next pure (cdcEventCheckpoint event)+                    token <- timeout 2000000 next >>= maybe (fail "no semisync transaction") pure+                    equal "one complete replicated row" 1 =<< readIORef rowsSeen+                    pure token+                insert stream n = do+                    done <- newEmptyMVar+                    bracket (forkFinally (withConnect open (\writer -> exec writer ("INSERT INTO changes VALUES(" ++ show n ++ ")")))+                        (putMVar done)) killThread $ \_ -> do+                            token <- consume stream+                            when (mode == AcknowledgeManually) $ do+                                pending <- tryReadMVar done+                                assert (not (isJust pending)) "writer completed before explicit acknowledgement"+                                cdcAcknowledge stream token+                                cdcAcknowledge stream token -- Idempotence must not disturb either sequence.+                            result <- timeout 2000000 (takeMVar done) >>= maybe (fail "writer did not resume after ACK") pure+                            either throwIO pure result+                            pure token+                initial source = getLastBinLogTracker source >>= maybe (fail "missing fixture binlog") (pure . FromBinLog)+            if resume then do+                saved <- newIORef Nothing+                forM_ [1..3::Int] $ \n -> withConnect compressed $ \source -> do+                    prior <- readIORef saved+                    start <- maybe (initial source) (pure . resumeFromCheckpoint) prior+                    stream <- openCDC source 610128 (options start)+                    token <- insert stream n+                    writeIORef saved (Just token)+                    cdcClose stream+              else if mode == AcknowledgeOnRead then withConnect compressed $ \source -> do+                -- On-read acknowledgement is intentionally confined to the+                -- legacy packet API; openCDC requires explicit durability.+                tracker <- getLastBinLogTracker source >>= maybe (fail "missing fixture binlog") pure+                (_,_,packets) <- dumpBinLog source 610128 tracker True+                let nextCommit = do+                        event <- S.read packets >>= maybe (fail "EOF before legacy commit") pure+                        unless (blEventType event == BINLOG_XID_EVENT) nextCommit+                forM_ [1..3::Int] $ \n -> do+                    done <- newEmptyMVar+                    bracket (forkFinally (withConnect open (\writer -> exec writer ("INSERT INTO changes VALUES(" ++ show n ++ ")")))+                        (putMVar done)) killThread $ \_ -> do+                            void (timeout 2000000 nextCommit >>= maybe (fail "no legacy commit") pure)+                            result <- timeout 2000000 (takeMVar done) >>= maybe (fail "legacy ACK did not release writer") pure+                            either throwIO pure result+              else withConnect compressed $ \source -> do+                start <- initial source+                stream <- openCDC source 610128 (options start)+                forM_ [1..3::Int] (void . insert stream)+                cdcClose stream+            withConnect open $ \observer -> do+                equal "three stored rows" 3 =<< scalarInt observer "SELECT COUNT(*) FROM changes"+                equal "stored values intact" 6 =<< scalarInt observer "SELECT SUM(id) FROM changes"+                (_,values) <- query_ observer "SHOW GLOBAL STATUS WHERE Variable_name IN ('Rpl_semi_sync_master_yes_tx','Rpl_semi_sync_master_no_tx')"+                statuses <- S.toList values+                equal "all commits acknowledged; no timeout fallback"+                    [[MySQLText "Rpl_semi_sync_master_no_tx",MySQLText "0"],+                     [MySQLText "Rpl_semi_sync_master_yes_tx",MySQLText "3"]] statuses
+ validation/hs/Validation/Stage7.hs view
@@ -0,0 +1,435 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE ForeignFunctionInterface #-}+-- | Independent authentication packets plus a real disposable PAM/Kerberos+-- server. Authentication inventory entries call these same live feature checks.+module Validation.Stage7 (cases, pamFeature, gssapiFeature) where++import Control.Concurrent+import Control.Exception hiding (assert)+import Control.Monad+import Data.Binary (encode)+import qualified Data.ByteString as B+import qualified Data.ByteString.Char8 as BC+import qualified Data.ByteString.Lazy as L+import Data.List (isInfixOf)+import Data.IORef+import Database.MySQL.Base+import qualified Database.MySQL.Connection as C+import qualified Database.MySQL.TLS as TLS+import Validation.ExternalAuthFixture+import Foreign.C.Types (CUInt(..))+import qualified Network.Socket as N+import qualified Network.Socket.ByteString as NB+import System.Environment+import qualified System.IO.Streams as S+import System.Timeout+import Validation.Common++foreign import ccall unsafe "mariadb_gss_live_contexts"+    nativeContextCount :: IO CUInt++awaitReleasedContexts :: IO ()+awaitReleasedContexts = do+    released <- timeout 2000000 wait+    equal "native GSS contexts released" (Just ()) released+  where+    wait = do+        count <- nativeContextCount+        unless (count == 0) (threadDelay 1000 >> wait)++case7 :: String -> IO () -> Case+case7 name = Case ("stage7." ++ name) "stage7-external-authentication"++packet :: Int -> B.ByteString -> Packet+packet n body = Packet (fromIntegral (B.length body)) (fromIntegral n) (L.fromStrict body)++ok :: Int -> Packet+ok n = packet n "\0\0\0\2\0\0\0"++switch :: B.ByteString -> B.ByteString -> Packet+switch plugin body = packet 2 ("\254" <> plugin <> "\0" <> body)++-- Packet two has already been read by the constructor. Remaining packets and+-- every client reply go through the production sequence validator.+conversation :: AuthenticationOptions -> Bool -> Packet -> [Packet]+             -> IO (IO (), IO [B.ByteString])+conversation auth secure first rest = do+    source <- S.fromList rest+    sent <- newIORef []+    conn <- C.newConnection defaultConnectionOptions source+        (\p -> modifyIORef' sent (L.toStrict (pBody p):)) (pure ())+    C.setExchangeSequence conn (fromIntegral (pSeqN first)+1)+    let hello = Greeting 10 "12.3.3-MariaDB" 1 "12345678" clientCap 224 2+            "901234567890" "mysql_native_password"+    pure (C.completeAuthWithOptions auth conn hello secure "unused SQL password" first,+          reverse <$> readIORef sent)++pam :: (DialogPrompt -> IO B.ByteString) -> AuthenticationOptions+pam callback = defaultAuthenticationOptions {authenticationDialog=Just callback}++rejectAuth :: IO a -> IO ()+rejectAuth action = do+    result <- try action+    case result of+        Left (_ :: AuthException) -> pure ()+        Right _ -> fail "invalid authentication unexpectedly succeeded"++-- Check displayed exceptions as well as rejection: rejecting a packet is not+-- sufficient if the error would copy an opaque token into application logs.+redactedAuth :: String -> IO () -> IO ()+redactedAuth marker action = do+    result <- try action+    case result of+        Left (err :: SomeException) -> do+            assert (not (marker `isInfixOf` displayException err)) "authentication data disclosed"+            case fromException err of+                Just (_ :: AuthException) -> pure ()+                Nothing -> fail "malformed authentication did not produce AuthException"+        Right () -> fail "malformed authentication unexpectedly succeeded"++-- Only the header exists. Reading a body is a test failure, rather than a large+-- allocation that could accidentally make a post-read check appear sufficient.+headerOnly :: ConnectionOptions -> Int -> IO (MySQLConn, IO Bool)+headerOnly options size = do+    supplied <- newIORef False+    bodyRead <- newIORef False+    input <- S.makeInputStream $ do+        previous <- atomicModifyIORef' supplied (\old -> (True,old))+        if previous then writeIORef bodyRead True >> fail "authentication body requested"+        else pure (Just (B.pack [fromIntegral size,fromIntegral (size `div` 256),+            fromIntegral (size `div` 65536),4]))+    (conn,_) <- C.newAuthenticatingTransportConnection options input (const (pure ())) (pure ())+    C.setExchangeSequence conn 4+    pure (conn,readIORef bodyRead)++rejectHeader :: IO a -> IO Bool -> IO ()+rejectHeader action bodyRead = do+    result <- try (void action)+    case result of+        Left (ProtocolError _) -> pure ()+        Left err -> throwIO err+        Right () -> fail "oversized authentication header accepted"+    equal "authentication header rejected before body read" False =<< bodyRead++fixturePassword :: IO B.ByteString+fixturePassword = BC.pack <$> getEnv "MB_AUTH_PAM_PASSWORD"++pamFeature :: IO ()+pamFeature = do+    password <- fixturePassword+    withConnect (pamConnect "mariadb_pam" (passwordDialog password)) healthy++gssapiFeature :: IO ()+gssapiFeature = withConnect gssapiConnect healthy++withCache :: String -> IO a -> IO a+withCache cache action = bracket (lookupEnv "KRB5CCNAME") restore $ \_ ->+    setEnv "KRB5CCNAME" cache >> action+  where+    restore Nothing = unsetEnv "KRB5CCNAME"+    restore (Just previous) = setEnv "KRB5CCNAME" previous++cases :: [Case]+cases =+    [ case7 "dialog.multiple-questions" $ do+        observed <- newIORef []+        let callback prompt = do+                modifyIORef' observed (prompt:)+                pure (if dialogEcho prompt then "code" else "password")+        (run,sent) <- conversation (pam callback) True+            (switch "dialog" "\4Password:") [packet 4 "\1\3Code:\0",ok 6]+        run+        equal "prompt kinds/text/rounds" [DialogPrompt "Password:" False False 0,+            DialogPrompt "Code:" True True 1] . reverse =<< readIORef observed+        equal "wire replies" ["password\0","code\0"] =<< sent+    , case7 "dialog.empty-switch-acknowledged" $ do+        (run,sent) <- conversation (pam (passwordDialog "secret")) True+            (switch "dialog" B.empty) [packet 4 "\1\5Password:",ok 6]+        run+        equal "empty switch ACK precedes credential" [B.empty,"secret\0"] =<< sent+    , case7 "dialog.nonlast-question-then-ok" $ do+        (run,_) <- conversation (pam (passwordDialog "secret")) True+            (switch "dialog" "\4Password:") [ok 4]+        run+    , case7 "dialog.empty-password" $ do+        (run,sent) <- conversation (pam (passwordDialog B.empty)) True+            (switch "dialog" "\5\0") [ok 4]+        run+        equal "empty password has terminator" ["\0"] =<< sent+    , case7 "dialog.plaintext-rejected-before-callback" $ do+        called <- newIORef False+        (run,sent) <- conversation (pam (\_ -> writeIORef called True >> pure "secret")) False+            (switch "dialog" "\5Password:") []+        rejectAuth run+        equal "no callback" False =<< readIORef called+        equal "no secret sent" [] =<< sent+    , case7 "dialog.explicit-opt-in-required" $ do+        (run,sent) <- conversation defaultAuthenticationOptions True (switch "dialog" "\5Password:") []+        rejectAuth run+        equal "no response" [] =<< sent+    , case7 "dialog.bad-kind" $ do+        (run,_) <- conversation (pam (passwordDialog "secret")) True (switch "dialog" "\6Password:") []+        rejectAuth run+    , case7 "dialog.embedded-nul-prompt" $ do+        (run,_) <- conversation (pam (passwordDialog "secret")) True (switch "dialog" "\5bad\0prompt\0") []+        rejectAuth run+    , case7 "dialog.embedded-nul-response" $ do+        (run,sent) <- conversation (pam (\_ -> pure "secret\0extra")) True (switch "dialog" "\5Password:") []+        rejectAuth run+        equal "no truncated credential sent" [] =<< sent+    , case7 "dialog.oversized-response" $ do+        (run,sent) <- conversation (pam (\_ -> pure (B.replicate 65536 65))) True (switch "dialog" "\5Password:") []+        rejectAuth run+        equal "no oversized credential sent" [] =<< sent+    , case7 "dialog.oversized-prompt" $ do+        (run,_) <- conversation (pam (passwordDialog "secret")) True+            (switch "dialog" (B.singleton 5 <> B.replicate 65536 65)) []+        rejectAuth run+    , case7 "dialog.final-question-requires-ok" $ do+        (run,_) <- conversation (pam (passwordDialog "secret")) True+            (switch "dialog" "\5Password:") [packet 4 "\4Another question:"]+        expectRejection run+    , case7 "dialog.continuation-wrapper-required" $ do+        (run,_) <- conversation (pam (\_ -> pure "response")) True+            (switch "dialog" "\4Question:") [packet 4 "\4Unwrapped:"]+        expectRejection run+    , case7 "dialog.wrapped-ok-is-not-terminal" $ do+        (run,_) <- conversation (pam (passwordDialog "secret")) True+            (switch "dialog" "\5Password:") [packet 4 "\1\0\0\0\2\0\0\0"]+        expectRejection run+    , case7 "dialog.unexpected-response-does-not-disclose-token" $ do+        let secretMarker = "credential-material-must-not-appear"+        (run,_) <- conversation (pam (passwordDialog "secret")) True+            (switch "dialog" "\5Password:") [packet 4 ("\1" <> secretMarker)]+        result <- try run+        case result of+            Left (err :: SomeException) -> do+                assert (not (BC.unpack secretMarker `isInfixOf` show err))+                    "authentication exception disclosed raw packet/token bytes"+                case fromException err of+                    Just (_ :: AuthException) -> pure ()+                    Nothing -> fail "unexpected authentication response lacks bounded AuthException"+            Right () -> fail "malformed authentication unexpectedly succeeded"+    , case7 "dialog.callback-failure-propagates" $ do+        (run,_) <- conversation (pam (\_ -> throwIO ThreadKilled)) True (switch "dialog" "\5Password:") []+        result <- try run+        case result of+            Left ThreadKilled -> pure ()+            _ -> fail "callback cancellation was swallowed"+    , case7 "dialog.wrapped-err-token-is-redacted" $ do+        (run,_) <- conversation (pam (passwordDialog "secret")) True+            (switch "dialog" "\4Password:") [packet 4 "\1\255\21\4#28000PRIVATE-TOKEN-MARKER"]+        redactedAuth "PRIVATE-TOKEN-MARKER" run+    , case7 "dialog.initial-err-token-is-redacted" $ do+        (run,_) <- conversation (pam (passwordDialog "secret")) True+            (switch "dialog" "\255\21\4#28000PRIVATE-TOKEN-MARKER") []+        redactedAuth "PRIVATE-TOKEN-MARKER" run+    , case7 "dialog.malformed-ok-is-redacted" $ do+        (run,_) <- conversation (pam (passwordDialog "secret")) True+            (switch "dialog" "\5Password:") [packet 4 "\0\255PRIVATE-TOKEN-MARKER"]+        redactedAuth "PRIVATE-TOKEN-MARKER" run+    , case7 "dialog.malformed-err-is-redacted" $ do+        (run,_) <- conversation (pam (passwordDialog "secret")) True+            (switch "dialog" "\5Password:") [packet 4 "\255\0\0#Z!Z!"]+        redactedAuth "Z!Z!" run+    , case7 "dialog.raw-server-err-preserved" $ do+        (run,_) <- conversation (pam (passwordDialog "secret")) True+            (switch "dialog" "\5Password:") [packet 4 "\255\21\4#28000ordinary rejection"]+        expectERR [1045] run+    , case7 "dialog.callback-decode-exception-preserved" $ do+        (run,_) <- conversation (pam (\_ -> throwIO (DecodePacketFailed "callback-owned" 0 "callback failure"))) True+            (switch "dialog" "\5Password:") []+        result <- try run+        case result of+            Left (DecodePacketFailed bytes _ _) -> equal "callback owns exception" "callback-owned" bytes+            Right () -> fail "callback exception swallowed"+    , case7 "dialog.round-limit" $ do+        count <- newIORef (0::Int)+        (run,_) <- conversation (pam (\_ -> modifyIORef' count (+1) >> pure "response")) True+            (switch "dialog" "\4Question:") [packet n "\1\4Question:" | n <- [4,6..100]]+        rejectAuth run+        actual <- readIORef count+        assert (actual <= 16) "unbounded callback loop"+    , case7 "dialog.password-helper-rejects-second-factor" $ do+        expectRejection (passwordDialog "password" (DialogPrompt "PIN:" False True 1))+        expectRejection (passwordDialog "password" (DialogPrompt "Username:" True True 0))+    , case7 "clear-password.secure-switch" $ do+        (run,sent) <- conversation (pam (passwordDialog "secret")) True+            (switch "mysql_clear_password" B.empty) [ok 4]+        run+        equal "clear password response" ["secret\0"] =<< sent+    , case7 "clear-password.plaintext-rejected" $ do+        (run,sent) <- conversation (pam (passwordDialog "secret")) False+            (switch "mysql_clear_password" B.empty) []+        rejectAuth run+        equal "no clear password" [] =<< sent+    , case7 "clear-password.oversized-challenge-rejected" $ do+        called <- newIORef False+        (run,sent) <- conversation (pam (\_ -> writeIORef called True >> pure "secret")) True+            (switch "mysql_clear_password" (B.replicate 1048577 65)) []+        rejectAuth run+        equal "oversized challenge did not invoke callback" False =<< readIORef called+        equal "no credential reply" [] =<< sent+    , case7 "wire.initial-header-limit" $ do+        (conn,bodyRead) <- headerOnly defaultConnectionOptions (2*1024*1024)+        rejectHeader (C.readConnPacket conn) bodyRead+    , case7 "wire.continuation-header-limit" $ do+        (conn,bodyRead) <- headerOnly defaultConnectionOptions (2*1024*1024)+        C.setExchangeSequence conn 3+        let hello = Greeting 10 "12.3.3-MariaDB" 1 "12345678" clientCap 224 2+                "901234567890" "mysql_native_password"+        rejectHeader (C.completeAuthWithOptions (pam (passwordDialog "secret")) conn hello True ""+            (switch "dialog" "\4Password:")) bodyRead+    , case7 "wire.smaller-caller-budget" $ do+        (conn,bodyRead) <- headerOnly defaultConnectionOptions {maximumMessageBytes=512} 1024+        rejectHeader (C.readConnPacket conn) bodyRead+    , case7 "wire.query-budget-restored-after-auth" $ do+        let payload = B.replicate (2*1024*1024) 65+        input <- S.fromList [L.toStrict (encode (ok 4)),L.toStrict (encode (packet 5 payload))]+        (authConn,conn) <- C.newAuthenticatingTransportConnection defaultConnectionOptions input (const (pure ())) (pure ())+        C.setExchangeSequence authConn 3+        let hello = Greeting 10 "12.3.3-MariaDB" 1 "12345678" clientCap 224 2+                "901234567890" "mysql_native_password"+        C.completeAuthWithOptions (pam (passwordDialog "secret")) authConn hello True ""+            (switch "dialog" "\5Password:")+        reply <- C.readConnPacket conn+        equal "ordinary reader admits larger query result" (L.fromStrict payload) (pBody reply)+    , case7 "gssapi.principal-opt-in-required" $ do+        (run,sent) <- conversation defaultAuthenticationOptions False+            (switch "auth_gssapi_client" "mariadb/localhost@TEST\0\0") []+        rejectAuth run+        equal "no ticket disclosure" [] =<< sent+    , case7 "gssapi.wrong-principal" $ do+        let auth = defaultAuthenticationOptions {authenticationGSSAPIPrincipal=Just "mariadb/expected@TEST"}+        (run,sent) <- conversation auth False (switch "auth_gssapi_client" "mariadb/unexpected@TEST\0\0") []+        rejectAuth run+        equal "no ticket disclosure" [] =<< sent+    , case7 "gssapi.native-ok-cannot-bypass-required-principal" $ do+        let auth = defaultAuthenticationOptions {authenticationGSSAPIPrincipal=Just "mariadb/localhost@TEST"}+        (run,_) <- conversation auth False (ok 2) []+        rejectAuth run+    , case7 "gssapi.initial-password-proof-is-suppressed" $+        bracket (N.socket N.AF_INET N.Stream 0) N.close $ \listener -> do+            N.bind listener (N.SockAddrInet 0 (N.tupleToHostAddress (127,0,0,1)))+            N.listen listener 1+            N.SockAddrInet port _ <- N.getSocketName listener+            captured <- newEmptyMVar+            finished <- newEmptyMVar+            let hello = Greeting 10 "12.3.3-MariaDB" 1 "12345678" clientCap 224 2+                    "901234567890" "mysql_native_password"+                receive sock size = do+                    -- Read the fixed header first, then exactly its payload;+                    -- no decoder under test determines the password offset.+                    let exact 0 parts = pure (B.concat (reverse parts))+                        exact remaining parts = do+                            chunk <- NB.recv sock remaining+                            when (B.null chunk) (fail "truncated client authentication")+                            exact (remaining-B.length chunk) (chunk:parts)+                    exact size []+            worker <- forkFinally (bracket (fst <$> N.accept listener) N.close $ \sock -> do+                NB.sendAll sock (L.toStrict (encode (encodeToPacket 0 hello)))+                header <- receive sock 4+                let size = sum [fromIntegral (B.index header n) * (256^n) | n <- [0..2]]+                body <- receive sock size+                putMVar captured body+                NB.sendAll sock (L.toStrict (encode (ok 2)))) (putMVar finished)+            flip finally (killThread worker) $ do+                let auth = defaultAuthenticationOptions {authenticationGSSAPIPrincipal=Just "mariadb/localhost@TEST"}+                    ci = defaultConnectInfoMB4 {ciHost="127.0.0.1",ciPort=port,+                        ciUser="gssapi-test",ciPassword="must-not-be-used"}+                rejectAuth (connectWithAuthentication defaultConnectionOptions {connectTimeoutMicros=1000000} auth ci)+                result <- timeout 1000000 (takeMVar finished)+                case result of+                    Just (Right ()) -> pure ()+                    _ -> fail "authentication wire fixture did not complete"+                body <- takeMVar captured+                let afterUser = B.drop 1 (snd (B.break (==0) (B.drop 32 body)))+                assert (not (B.null afterUser)) "client authentication payload missing"+                equal "no native password proof before GSSAPI" 0 (B.head afterUser)+    , case7 "live.gssapi-rejects-password-only-success" $ do+        ci <- config+        let auth = defaultAuthenticationOptions {authenticationGSSAPIPrincipal=Just "mariadb/localhost@TEST"}+        expectRejection (withConnect (connectWithAuthentication defaultConnectionOptions auth ci) healthy)+    , case7 "gssapi.oversized-announcement" $ do+        let auth = defaultAuthenticationOptions {authenticationGSSAPIPrincipal=Just "mariadb/localhost@TEST"}+        (run,_) <- conversation auth False (switch "auth_gssapi_client" (B.replicate 1048577 65)) []+        rejectAuth run+    , case7 "gssapi.malformed-announcement" $ forM_+        ["", "mariadb/localhost@TEST", "mariadb/localhost@TEST\0NTLM\0", "mariadb/localhost@TEST\0\0extra"] $ \challenge -> do+        let auth = defaultAuthenticationOptions {authenticationGSSAPIPrincipal=Just "mariadb/localhost@TEST"}+        (run,_) <- conversation auth False (switch "auth_gssapi_client" challenge) []+        rejectAuth run+    , case7 "live.pam-wrong-password" $+        expectERR [1045] (withConnect (pamConnect "mariadb_pam" (passwordDialog "incorrect")) healthy)+    , case7 "live.pam-multiple-questions" $ do+        password <- fixturePassword+        code <- BC.pack <$> getEnv "MB_AUTH_PAM_CODE"+        prompts <- newIORef []+        let callback prompt = modifyIORef' prompts (prompt:) >>+                pure (if dialogRound prompt == 0 then password else code)+        withConnect (pamConnect "mariadb_pam_multi" callback) healthy+        actual <- reverse <$> readIORef prompts+        equal "real PAM conversation length" 2 (length actual)+        equal "real PAM echo modes" [False,True] (map dialogEcho actual)+    , case7 "live.pam-wrong-second-factor" $ do+        password <- fixturePassword+        let callback prompt = pure (if dialogRound prompt == 0 then password else "incorrect")+        expectERR [1045] (withConnect (pamConnect "mariadb_pam_multi" callback) healthy)+    , case7 "live.pam-cancellation-and-reconnect" $ do+        ca <- getEnv "MB_AUTH_TLS_CA"+        ci <- authConfig "mariadb_pam"+        params <- TLS.makeClientParams (TLS.CustomCAStore ca)+        let options = defaultConnectionOptions {connectTimeoutMicros=200000}+        result <- try (timeout 1000000 (TLS.connectWithAuthentication options+            (pam (\_ -> threadDelay 1000000 >> pure "never sent")) ci (params,"localhost")))+        case result of+            Left (_ :: AuthException) -> pure ()+            Right Nothing -> fail "connection deadline failed to release PAM callback"+            Right (Just conn) -> close conn >> fail "timed out PAM unexpectedly connected"+        pamFeature+    , case7 "live.gssapi-tls" $ do+        ci <- authConfig "mariadb_gssapi"+        principal <- BC.pack <$> getEnv "MB_AUTH_GSSAPI_PRINCIPAL"+        ca <- getEnv "MB_AUTH_TLS_CA"+        params <- TLS.makeClientParams (TLS.CustomCAStore ca)+        withConnect (TLS.connectWithAuthentication defaultConnectionOptions+            defaultAuthenticationOptions {authenticationGSSAPIPrincipal=Just principal}+            ci (params,"localhost")) healthy+    , case7 "live.gssapi-premature-ok-rejected" $ do+        principal <- BC.pack <$> getEnv "MB_AUTH_GSSAPI_PRINCIPAL"+        let auth = defaultAuthenticationOptions {authenticationGSSAPIPrincipal=Just principal}+        (run,_) <- conversation auth False (switch "auth_gssapi_client" (principal <> "\0\0")) [ok 4]+        rejectAuth run+        awaitReleasedContexts+    , case7 "live.gssapi-wrong-identity" $ do+        cache <- getEnv "MB_AUTH_GSSAPI_OTHER_CACHE"+        withCache cache (expectERR [1045] (withConnect gssapiConnect healthy))+    , case7 "live.gssapi-missing-cache" $+        withCache "FILE:/nonexistent/mariadb-haskell-stage7-cache" (expectRejection (withConnect gssapiConnect healthy))+    , case7 "live.gssapi-repeated-context-cleanup" $ do+        replicateM_ 20 gssapiFeature+        awaitReleasedContexts+    , case7 "live.gssapi-cancel-before-server-token" $ do+        principal <- BC.pack <$> getEnv "MB_AUTH_GSSAPI_PRINCIPAL"+        entered <- newEmptyMVar+        blocked <- newEmptyMVar+        source <- S.makeInputStream (putMVar entered () >> takeMVar blocked)+        conn <- C.newConnection defaultConnectionOptions source (const (pure ())) (pure ())+        C.setExchangeSequence conn 3+        let hello = Greeting 10 "12.3.3-MariaDB" 1 "12345678" clientCap 224 2+                "901234567890" "mysql_native_password"+            auth = defaultAuthenticationOptions {authenticationGSSAPIPrincipal=Just principal}+        done <- newEmptyMVar+        worker <- forkFinally (C.completeAuthWithOptions auth conn hello False ""+            (switch "auth_gssapi_client" (principal <> "\0\0"))) (putMVar done)+        flip finally (killThread worker) $ do+            reached <- timeout 2000000 (takeMVar entered)+            equal "native initiator token produced before cancellation" (Just ()) reached+            equal "live context owned until cancellation" 1 =<< nativeContextCount+            throwTo worker ThreadKilled+            result <- timeout 1000000 (takeMVar done)+            case result of+                Just (Left err) | Just ThreadKilled <- fromException err -> pure ()+                _ -> fail "GSSAPI cancellation did not propagate promptly"+            awaitReleasedContexts+    ]
+ validation/hs/Validation/Stage7Bench.hs view
@@ -0,0 +1,43 @@+-- | Complete external-authentication setup costs. Each operation opens,+-- authenticates, initializes, verifies a query and closes a connection. The+-- fixture already owns a service-ticket cache; KDC acquisition is not timed.+module Validation.Stage7Bench (run) where++import Control.Exception (evaluate)+import Control.Monad+import qualified Data.ByteString.Char8 as BC+import Data.List (intercalate)+import Database.MySQL.Base+import GHC.Clock+import GHC.Stats+import System.Environment+import System.Mem+import Validation.Common+import Validation.ExternalAuthFixture (pamConnect, gssapiConnect)++run :: String -> Int -> IO ()+run ident count = do+    when (count <= 0) (fail "positive authentication benchmark count required")+    password <- BC.pack <$> getEnv "MB_AUTH_PAM_PASSWORD"+    code <- BC.pack <$> getEnv "MB_AUTH_PAM_CODE"+    let open = case ident of+            "auth.pam.password" -> pamConnect "mariadb_pam" (passwordDialog password)+            "auth.pam.multiple" -> pamConnect "mariadb_pam_multi" (\prompt ->+                pure (if dialogRound prompt == 0 then password else code))+            "auth.gssapi.cached-ticket" -> gssapiConnect+            _ -> fail "unknown authentication benchmark"+        operation = sum <$> replicateM count+            (withConnect open (\conn -> healthy conn >> pure (1 :: Integer)))+    void operation+    void operation+    performGC+    before <- getRTSStats+    start <- getMonotonicTimeNSec+    checksum <- operation >>= evaluate+    end <- getMonotonicTimeNSec+    performGC+    after <- getRTSStats+    equal "authenticated connections verified" (toInteger count) checksum+    putStrLn (intercalate "\t" ["STAGE7",ident,show count,show (end-start),+        show (allocated_bytes after-allocated_bytes before),show (cpu_ns after-cpu_ns before),+        show (gc_cpu_ns after-gc_cpu_ns before),show checksum])
+ validation/hs/Validation/Supplementary.hs view
@@ -0,0 +1,90 @@+{-# LANGUAGE ScopedTypeVariables #-}+module Validation.Supplementary (run) where++import Control.Exception (evaluate)+import Control.Monad+import Data.List (isPrefixOf)+import Data.IORef+import qualified Data.ByteString as B+import GHC.Clock+import GHC.Stats+import System.Mem+import Database.MySQL.Base+import qualified System.IO.Streams as S+import Validation.Common++-- Separate experiment: no change to any frozen benchmark function or enumeration.+run :: String -> Int -> IO ()+run ident n = case ident of+    "connect.tcp" -> measured (fromIntegral n) $ replicateM_ n (withConn (const (pure ()))) >> pure (fromIntegral n)+    "parameters.select" -> withConn $ \c -> measured (fromIntegral n) $ loop n (param c)+    "parameters.mixed" -> withConn $ \c -> measured (fromIntegral n) $ loop n (scalarInt c "SELECT 3" >> param c)+    "transaction.empty" -> withConn $ \c -> measured (fromIntegral n) $ loop n (withTransaction c (pure 7))+    "batch.1000" -> withConn $ \c -> do+        exec c "CREATE TEMPORARY TABLE mb_stage1_batch(n INT)"+        let action = do+                exec c "TRUNCATE mb_stage1_batch"+                void (executeMany c "INSERT INTO mb_stage1_batch VALUES(?)" [[MySQLInt32 x] | x <- [1..1000]])+                scalarInt c "SELECT SUM(n) FROM mb_stage1_batch"+        measured (fromIntegral (n*1000)) (loop n action)+    "stream.retention" -> withConn $ \c -> measured (fromIntegral n) $ do+        (_,stream) <- query_ c (sql ("SELECT seq,REPEAT('x',128) FROM seq_1_to_" ++ show n))+        S.fold (\a row -> a + sum [number v | v <- take 1 row]) 0 stream+    "retention.tiny-slice" -> withConn $ \c -> do+        retained <- newIORef B.empty+        (_,stream) <- query_ c (sql ("SELECT CAST('x' AS BINARY),CAST(REPEAT('y',"++show n++") AS BINARY)"))+        start <- getMonotonicTimeNSec+        let saveFirst = do+                value <- S.read stream+                case value of+                    Just (MySQLBytes small:_) -> writeIORef retained small+                    _ -> fail "tiny slice shape"+        saveFirst+        S.skipToEof stream+        performGC+        stats <- getRTSStats+        small <- readIORef retained+        equal "tiny slice retained exactly" "x" small+        end <- getMonotonicTimeNSec+        putStrLn ("RETENTION\t"++ident++"\t"++show n++"\t"++show (end-start)++"\t"++show (gcdetails_live_bytes (gc stats))++"\t1")+    _ | "retention." `isPrefixOf` ident -> withConn $ \c -> do+        (_,stream) <- query_ c (sql ("SELECT seq,REPEAT('x',128) FROM seq_1_to_" ++ show n))+        let consume count total = do+                next <- S.read stream+                case next of+                    Nothing -> pure total+                    Just values -> do+                        when (count `mod` 10000==0) $ do+                            performGC+                            stats <- getRTSStats+                            putStrLn ("LIVE_HEAP\t"++show count++"\t"++show (gcdetails_live_bytes (gc stats)))+                        value <- case values of x:_ -> pure (number x); [] -> fail "empty retention row"+                        value `seq` total `seq` consume (count+1) (total+value)+        start <- getMonotonicTimeNSec+        checksum <- consume (0::Int) 0+        end <- getMonotonicTimeNSec+        equal "stream retention checksum" (toInteger n*(toInteger n+1) `div` 2) checksum+        performGC+        stats <- getRTSStats+        putStrLn ("RETENTION\t"++ident++"\t"++show n++"\t"++show (end-start)++"\t"++show (gcdetails_live_bytes (gc stats))++"\t"++show checksum)+      | otherwise -> fail ("unknown supplementary profile: " ++ ident)+  where+    param c = do+        (_,stream) <- query c "SELECT ?" [MySQLInt32 7]+        xs <- S.toList stream+        case xs of [[x]] -> pure (number x); _ -> fail "parameter checksum shape"+    loop count action = sum <$> replicateM count action+    measured :: Integer -> IO Integer -> IO ()+    measured units action = do+        -- Two warm-ups with the same operation, then one measured region.+        void action; void action+        performGC+        before <- getRTSStats+        start <- getMonotonicTimeNSec+        checksum <- action >>= evaluate+        end <- getMonotonicTimeNSec+        performGC+        after <- getRTSStats+        putStrLn ("STAGE1\t" ++ ident ++ "\t" ++ show (units :: Integer) ++ "\t" ++ show (end-start)+            ++ "\t" ++ show (allocated_bytes after-allocated_bytes before)+            ++ "\t" ++ show (max_live_bytes after) ++ "\t" ++ show checksum)
+ validation/hs/Validation/WireProxy.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE ScopedTypeVariables #-}+-- | Transparent byte counters for real compressed/plain server exchanges.+-- Neither TLS nor MariaDB framing is decoded or rewritten by this proxy.+module Validation.WireProxy (withWireProxy) where++import Control.Concurrent+import Control.Exception+import Control.Monad+import Data.IORef+import qualified Data.ByteString as B+import qualified Network.Socket as N+import qualified Network.Socket.ByteString as NS+import System.Timeout+import Database.MySQL.Base (ConnectInfo(..))++-- | Counters are (bytes sent to MariaDB, bytes received from MariaDB). Updates+-- happen before forwarding, so a fully consumed reply has already been charged.+-- The proxy accepts one connection and joins every worker during cleanup.+withWireProxy :: ConnectInfo -> (ConnectInfo -> IO (Integer,Integer) -> IO a) -> IO a+withWireProxy ci action = bracket listener N.close $ \server -> do+    N.SockAddrInet port _ <- N.getSocketName server+    counters <- newIORef (0,0)+    finished <- newEmptyMVar+    let serve = bracket (fst <$> N.accept server) N.close $ \client -> do+            addresses <- N.getAddrInfo (Just N.defaultHints {N.addrSocketType=N.Stream})+                (Just (ciHost ci)) (Just (show (ciPort ci)))+            address <- case addresses of first:_ -> pure first; [] -> fail "proxy backend address unavailable"+            bracket (N.socket (N.addrFamily address) N.Stream N.defaultProtocol) N.close $ \backend -> do+                N.connect backend (N.addrAddress address)+                done <- newEmptyMVar+                let forward upstream from to = do+                        bytes <- NS.recv from 32768+                        unless (B.null bytes) $ do+                            atomicModifyIORef' counters $ \(sent,received) ->+                                let size = toInteger (B.length bytes)+                                in (if upstream then (sent+size,received) else (sent,received+size),())+                            NS.sendAll to bytes+                            forward upstream from to+                mask $ \restore -> do+                    firstDone <- newEmptyMVar+                    secondDone <- newEmptyMVar+                    first <- forkFinally (restore (forward True client backend))+                        (\_ -> putMVar firstDone () >> void (tryPutMVar done ()))+                    second <- forkFinally (restore (forward False backend client))+                        (\_ -> putMVar secondDone () >> void (tryPutMVar done ())) `onException` killThread first+                    let cleanup = do+                            killThread first+                            killThread second+                            joined <- timeout 1000000 (takeMVar firstDone >> takeMVar secondDone)+                            when (joined == Nothing) (fail "proxy forwarding workers did not finish")+                    restore (takeMVar done) `finally` cleanup+        stop worker = do+            killThread worker+            joined <- timeout 2000000 (takeMVar finished)+            when (joined == Nothing) (fail "proxy server did not finish")+    bracket (forkFinally serve (const (putMVar finished ()))) stop $ \_ ->+        action ci {ciHost="127.0.0.1",ciPort=port} (readIORef counters)+  where+    listener = bracketOnError (N.socket N.AF_INET N.Stream N.defaultProtocol) N.close $ \sock -> do+        N.setSocketOption sock N.ReuseAddr 1+        N.bind sock (N.SockAddrInet 0 (N.tupleToHostAddress (127,0,0,1)))+        N.listen sock 1+        pure sock
+ validation/profile_workloads.py view
@@ -0,0 +1,89 @@+#!/usr/bin/env python3+"""Run focused experiments using the unchanged frozen workload functions.++These development measurements never replace the complete live report. A failed+build/workload or a source edit during measurement invalidates the experiment.+"""+import argparse+import json+import os+from pathlib import Path+import statistics++import publish_results as live+import release_recorder as run+++def main():+    parser = argparse.ArgumentParser(description=__doc__)+    parser.add_argument("--profiles", nargs="+", required=True)+    parser.add_argument("--repetitions", type=int, default=7)+    parser.add_argument("--output", type=Path, required=True)+    args = parser.parse_args()+    if args.repetitions < 1:+        parser.error("repetitions must be positive")+    if args.output.exists():+        parser.error("experiment exists; preserve previous results")++    env = os.environ.copy()+    env.update(json.loads((run.HERE / ".local/environment.json").read_text()))+    env["TZ"] = "UTC"+    print("MariaDB:", run.sql_client(env, "SELECT VERSION()"), flush=True)+    inputs = live.input_snapshot()+    # run.command checks exit status: never measure a stale binary after a+    # failed build. Keep the build log beside the separate experiment output.+    build = run.command(+        ["cabal", "build", "exe:mariadb-validation", "-O2", "--enable-tests"],+        timeout=300,+    )+    args.output.parent.mkdir(parents=True, exist_ok=True)+    args.output.with_suffix(".build.log").write_text(build.stdout + build.stderr)+    if live.input_snapshot()["files"] != inputs["files"]:+        raise RuntimeError("inputs changed during profile build")+    binary = run.executable("exe:mariadb-validation")+    manifest = dict(+        line.split("\t")+        for line in run.command([binary, "list-benchmarks"]).stdout.splitlines()+    )+    unknown = set(args.profiles) - manifest.keys()+    if unknown:+        parser.error("unknown frozen workloads: " + ", ".join(sorted(unknown)))+    data = {+        "schema": 1,+        "recorded_at": live.now(),+        "inputs": inputs,+        "successful_fresh_build": True,+        "server": run.server_metadata(env),+        "profiles": {},+    }+    logs = args.output.with_suffix("")+    logs.mkdir()+    for ident in args.profiles:+        samples = [+            run.benchmark_sample(+                binary, ident, int(manifest[ident]), env,+                logs / f"{ident}.{rep}.log",+            )+            for rep in range(args.repetitions)+        ]+        if any(sample["status"] != "PASS" for sample in samples):+            raise RuntimeError("workload failed: " + ident)+        profile = {+            "samples": samples,+            "median_ns_per_unit": statistics.median(+                sample["ns_per_unit"] for sample in samples+            ),+            "median_allocated_bytes_per_unit": statistics.median(+                sample["allocated_bytes"] / sample["units"] for sample in samples+            ),+        }+        data["profiles"][ident] = profile+        print(ident, profile["median_ns_per_unit"],+              profile["median_allocated_bytes_per_unit"], flush=True)+    if live.input_snapshot()["files"] != inputs["files"]:+        raise RuntimeError("inputs changed during profile experiment")+    args.output.write_text(json.dumps(data, indent=2) + "\n")+++if __name__ == "__main__":+    main()
+ validation/provision.py view
@@ -0,0 +1,117 @@+#!/usr/bin/env python3+"""Provision only the reserved local validation databases and accounts.++Run after installing/starting/configuring MariaDB. Uses the current macOS user's+socket-admin account. It does not alter root or any application account.+"""+import argparse+import json+import os+from pathlib import Path+import secrets+import shutil+import subprocess+import time+import getpass++HERE = Path(__file__).resolve().parent+++def main():+    p = argparse.ArgumentParser(description=__doc__)+    p.add_argument("--client", default=shutil.which("mariadb") or "/opt/homebrew/opt/mariadb@12.3/bin/mariadb")+    p.add_argument("--socket", default="/tmp/mysql.sock")+    p.add_argument("--port", default="3306")+    p.add_argument("--tls-ca", default="/opt/homebrew/var/mariadb-haskell/tls/ca.pem")+    p.add_argument("--output", type=Path, help="separate private environment file for another local profile")+    args = p.parse_args()+    local = HERE / ".local"; local.mkdir(exist_ok=True)+    path = args.output or local / "environment.json"+    path.parent.mkdir(parents=True, exist_ok=True)+    config = json.loads(path.read_text()) if path.exists() else {"MB_PASSWORD": secrets.token_hex(20)}+    config.update(MB_HOST="127.0.0.1",MB_PORT=args.port,MB_USER="mariadb_haskell_test",+                  MB_DATABASE="mariadb_haskell_validation",MB_SOCKET=args.socket,+                  MB_TLS_CA=args.tls_ca,MB_TLS_NAME="localhost")+    tls=Path(args.tls_ca).parent+    config.update(MB_TLS_CLIENT_CERT=str(tls/"client-cert.pem"),+                  MB_TLS_CLIENT_KEY=str(tls/"client-key.pem"),+                  MB_TLS_EXPIRED_CERT=str(tls/"expired-client-cert.pem"),+                  MB_TLS_EXPIRED_KEY=str(tls/"expired-client-key.pem"))+    def admin(sql, check=True):+        return subprocess.run([args.client,"--protocol=socket","--socket="+args.socket,"--batch","--skip-column-names"],+                              input=sql,text=True,capture_output=True,check=check)+    for attempt in range(30):+        ready = admin("SELECT VERSION()",False)+        if ready.returncode == 0: break+        time.sleep(1)+    else: raise SystemExit("MariaDB must be running before provisioning: " + ready.stderr)+    if "MariaDB" not in ready.stdout: raise SystemExit("expected MariaDB server")+    if not Path(args.tls_ca).is_file(): raise SystemExit("configured TLS CA does not exist")+    password = config["MB_PASSWORD"].replace("\\","\\\\").replace("'","''")+    os_user=getpass.getuser().replace("'","''")+    admin(f"""+CREATE DATABASE IF NOT EXISTS mariadb_haskell_validation CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;+CREATE DATABASE IF NOT EXISTS testMySQLHaskell CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;+CREATE USER IF NOT EXISTS 'mariadb_haskell_test'@'localhost' IDENTIFIED BY '{password}';+ALTER USER 'mariadb_haskell_test'@'localhost' IDENTIFIED BY '{password}';+GRANT ALL ON mariadb_haskell_validation.* TO 'mariadb_haskell_test'@'localhost';+GRANT BINLOG MONITOR, REPLICATION SLAVE ON *.* TO 'mariadb_haskell_test'@'localhost';+CREATE USER IF NOT EXISTS 'testMySQLHaskell'@'localhost' IDENTIFIED BY '';+ALTER USER 'testMySQLHaskell'@'localhost' IDENTIFIED BY '';+GRANT ALL ON testMySQLHaskell.* TO 'testMySQLHaskell'@'localhost';+GRANT BINLOG MONITOR, REPLICATION SLAVE, CREATE USER ON *.* TO 'testMySQLHaskell'@'localhost';+CREATE USER IF NOT EXISTS 'mariadb_x509'@'localhost' IDENTIFIED BY '{password}' REQUIRE X509;+ALTER USER 'mariadb_x509'@'localhost' IDENTIFIED BY '{password}' REQUIRE X509;+GRANT ALL ON mariadb_haskell_validation.* TO 'mariadb_x509'@'localhost';+CREATE USER IF NOT EXISTS 'mariadb_socket'@'localhost' IDENTIFIED VIA unix_socket USING '{os_user}';+ALTER USER 'mariadb_socket'@'localhost' IDENTIFIED VIA unix_socket USING '{os_user}';+GRANT ALL ON mariadb_haskell_validation.* TO 'mariadb_socket'@'localhost';+""")+    # Separate role fixtures keep the ordinary benchmark account unchanged.+    # The default-role account reaches the database through its role only, so+    # restoring a different database before that role is an observable error.+    role = "mb_stage4_rôle_'`"+    role_identifier = "`" + role.replace("`", "``") + "`"+    admin(f"""+CREATE ROLE IF NOT EXISTS {role_identifier};+CREATE ROLE IF NOT EXISTS mb_stage4_other_role;+GRANT ALL ON mariadb_haskell_validation.* TO {role_identifier};+CREATE USER IF NOT EXISTS 'mariadb_role_none'@'localhost' IDENTIFIED BY '{password}';+ALTER USER 'mariadb_role_none'@'localhost' IDENTIFIED BY '{password}';+CREATE USER IF NOT EXISTS 'mariadb_role_default'@'localhost' IDENTIFIED BY '{password}';+ALTER USER 'mariadb_role_default'@'localhost' IDENTIFIED BY '{password}';+GRANT ALL ON mariadb_haskell_validation.* TO 'mariadb_role_none'@'localhost';+GRANT {role_identifier} TO 'mariadb_role_none'@'localhost';+GRANT mb_stage4_other_role TO 'mariadb_role_none'@'localhost';+GRANT {role_identifier} TO 'mariadb_role_default'@'localhost';+GRANT mb_stage4_other_role TO 'mariadb_role_default'@'localhost';+SET DEFAULT ROLE NONE FOR 'mariadb_role_none'@'localhost';+SET DEFAULT ROLE {role_identifier} FOR 'mariadb_role_default'@'localhost';+CREATE ROLE IF NOT EXISTS `mb_stage4_汉`;+CREATE ROLE IF NOT EXISTS `mb_stage4_字`;+GRANT ALL ON mariadb_haskell_validation.* TO `mb_stage4_汉`;+CREATE USER IF NOT EXISTS 'mariadb_role_wide'@'localhost' IDENTIFIED BY '{password}';+ALTER USER 'mariadb_role_wide'@'localhost' IDENTIFIED BY '{password}';+GRANT `mb_stage4_汉` TO 'mariadb_role_wide'@'localhost';+GRANT `mb_stage4_字` TO 'mariadb_role_wide'@'localhost';+SET DEFAULT ROLE `mb_stage4_汉` FOR 'mariadb_role_wide'@'localhost';+""")+    configured = []+    for soname,plugin,user in [("auth_ed25519","ed25519","mariadb_ed25519"),+                              ("auth_parsec","parsec","mariadb_parsec"),+                              ("auth_mysql_sha2","caching_sha2_password","mariadb_sha2")]:+        exists = admin(f"SELECT COUNT(*) FROM information_schema.PLUGINS WHERE PLUGIN_NAME='{plugin}' AND PLUGIN_STATUS='ACTIVE'").stdout.strip()+        install = admin(f"INSTALL SONAME '{soname}'",False) if exists == "0" else None+        if install and install.returncode:+            print(plugin,"unavailable:",install.stderr.strip())+            continue+        admin(f"CREATE USER IF NOT EXISTS '{user}'@'localhost' IDENTIFIED VIA {plugin} USING PASSWORD('{password}'); ALTER USER '{user}'@'localhost' IDENTIFIED VIA {plugin} USING PASSWORD('{password}'); GRANT ALL ON mariadb_haskell_validation.* TO '{user}'@'localhost';")+        configured.append(plugin)+    config["MB_AVAILABLE_AUTH"] = ",".join(configured)+    path.write_text(json.dumps(config,indent=2)+"\n"); path.chmod(0o600)+    print("Configured dedicated validation accounts on",ready.stdout.strip())+    print("Connection settings:",path,"(mode 0600; excluded from Git)")+    print("Authentication fixtures:",", ".join(configured))+++if __name__ == "__main__": main()
+ validation/publish_results.py view
@@ -0,0 +1,492 @@+#!/usr/bin/env python3+"""Publish the live report, or check that it still covers the current source."""+from __future__ import annotations++import argparse+from collections import Counter+import datetime as dt+import hashlib+from html import escape+import json+import os+from pathlib import Path+import subprocess+import tempfile++ROOT = Path(__file__).resolve().parents[1]+HERE = ROOT / "validation"+BASELINE = HERE / "baselines/macos-arm64-mariadb-12.3.3/results.json"+CURRENT = HERE / "results/current.json"+REPORT = HERE / "RESULTS.md"+OVERVIEW = CURRENT.parent / "overview.svg"+++def now():+    return dt.datetime.now(dt.timezone.utc).isoformat()+++def git(root, *args):+    return subprocess.check_output(["git", *args], cwd=root).decode()+++def is_input(path):+    p = Path(path)+    return (p.suffix in {".hs", ".hsc", ".c", ".h", ".cabal", ".nix"}+            or path.startswith(("test/", "npins/"))+            or (path.startswith("validation/") and p.suffix == ".py")+            or (path.startswith("tools/") and p.suffix == ".py")+            or path in {"ca-bundle.json", "LICENSE", "LICENSE-MPL-2.0"}+            or p.name.startswith("cabal.project") or path == "mozillaCAStore.pem"+            or path in {".dockerignore", "validation/linux/Dockerfile", "validation/auth_fixture/Dockerfile", "validation/auth_fixture/.dockerignore",+                        "validation/results/accepted.json", "validation/results/previous-stage.json",+                        "validation/results/stage7-reference.json",+                        "validation/results/references/stage6-publication-1.json",+                        "validation/results/experiments/stage7-runtime-equivalence.json",+                        "validation/results/experiments/stage7-authentication-image-config.json",+                        "validation/results/experiments/stage7-first-authentication.log",+                        "validation/results/experiments/stage7-runtime-identity-build.log",+                        "validation/baselines/macos-arm64-native-statements/results.json",+                        "validation/baselines/macos-arm64-native-statements/SUMMARY.md",+                        "validation/baselines/macos-arm64-native-statements/SHA256SUMS",+                        "validation/baselines/macos-arm64-session-reuse/results.json",+                        "validation/baselines/macos-arm64-session-reuse/SUMMARY.md",+                        "validation/baselines/macos-arm64-session-reuse/SHA256SUMS",+                        "validation/baselines/macos-arm64-cdc/results.json",+                        "validation/baselines/macos-arm64-cdc/SUMMARY.md",+                        "validation/baselines/macos-arm64-cdc/SHA256SUMS",+                        "validation/baselines/macos-arm64-wire-features/results.json",+                        "validation/baselines/macos-arm64-wire-features/SUMMARY.md",+                        "validation/baselines/macos-arm64-wire-features/SHA256SUMS",+                        "validation/baselines/macos-arm64-external-authentication/results.json",+                        "validation/baselines/macos-arm64-external-authentication/SUMMARY.md",+                        "validation/baselines/macos-arm64-external-authentication/SHA256SUMS"}+            or path.startswith(".github/workflows/"))+++def input_snapshot(root=ROOT):+    """Hash build/test inputs, including additions/deletions, but not reports."""+    paths = set(git(root, "ls-files", "-z", "--cached", "--others", "--exclude-standard").split("\0"))+    paths = sorted(p for p in paths if p and is_input(p))+    files = {p: hashlib.sha256((root / p).read_bytes()).hexdigest()+             if (root / p).is_file() else None for p in paths}+    changed = set(git(root, "diff", "--name-only", "-z", "HEAD").split("\0"))+    changed.update(git(root, "ls-files", "-z", "--others", "--exclude-standard").split("\0"))+    return {"commit": git(root, "rev-parse", "HEAD").strip(),+            "branch": git(root, "branch", "--show-current").strip(),+            "files": files, "dirty_inputs": sorted(set(paths) & changed),+            "sha256": hashlib.sha256(json.dumps(files, sort_keys=True).encode()).hexdigest()}+++def relative(path):+    try:+        return str(Path(path).resolve().relative_to(ROOT))+    except ValueError:+        return str(Path(path).resolve())+++def atomic_write(path, content):+    path.parent.mkdir(parents=True, exist_ok=True)+    with tempfile.NamedTemporaryFile(mode="w", dir=path.parent, delete=False) as f:+        temporary = Path(f.name)+        f.write(content)+    try:+        os.replace(temporary, path)+    finally:+        temporary.unlink(missing_ok=True)+++def compatible(current, baseline):+    return (current.get("comparison", {}).get("same_workloads") is True+            and current["machine"] == baseline["machine"]+            and current["server"] == baseline["server"]+            and current.get("connector_c_version") == baseline.get("connector_c_version")+            and all(current["build"][k] == baseline["build"][k] for k in ("ghc", "cabal"))+            and current["dependency_plan_sha256"] == baseline["dependency_plan_sha256"])+++def cell(value):+    return str(value).replace("|", "&#124;").replace("\n", " ").replace("\r", " ")+++def status_label(status):+    marker = {"PASS": "🟢", "FAIL": "🔴", "TIMEOUT": "🟠",+              "UNSUPPORTED": "🔵", "ENVIRONMENT_UNAVAILABLE": "🟡"}.get(status, "⚪")+    return f"{marker} {status}"+++def render_overview(document):+    """A local, accessible SVG; no remote badge service or CSS dependency."""+    attempt = document["latest_attempt"]+    completed = document.get("latest_completed_run")+    lines = ['<svg xmlns="http://www.w3.org/2000/svg" width="1040" height="300" viewBox="0 0 1040 300" role="img" aria-labelledby="title description">',+             '<title id="title">MariaDB validation overview</title>',+             '<rect x="1" y="1" width="1038" height="298" rx="18" fill="#0f172a" stroke="#334155"/>',+             '<g font-family="-apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">',+             '<text x="30" y="35" font-size="12" font-weight="700" letter-spacing="2" fill="#94a3b8">MARIADB / VALIDATION</text>']+    if not completed:+        lines += ['<desc id="description">The latest validation attempt stopped. No complete results are available.</desc>',+                  '<text x="30" y="94" font-size="30" font-weight="700" fill="#fb7185">No completed suite</text>',+                  f'<text x="30" y="137" font-size="17" fill="#cbd5e1">Stopped at {escape(attempt["stage"])}</text>', '</g>', '</svg>']+        return "\n".join(lines) + "\n"+    data = completed["results"]+    counts = Counter(c["status"] for c in data["correctness"])+    regressions = data.get("comparison", {}).get("regressions", [])+    label = "CURRENT COMPLETE RUN" if attempt["state"] == "COMPLETED" else "PREVIOUS COMPLETE RUN"+    comparison = f"{len(regressions)} comparison {'alert' if len(regressions) == 1 else 'alerts'}" if regressions else "No comparison regressions"+    if not data.get("comparison"):+        comparison = "Comparison not requested"+    color = "#fbbf24" if regressions or not data.get("comparison") else "#34d399"+    description = (f"{counts['PASS']} passing records, {counts['FAIL']} failures, "+                   f"{counts['TIMEOUT']} timeouts, {counts['UNSUPPORTED']} unsupported APIs and "+                   f"{counts['ENVIRONMENT_UNAVAILABLE']} environment gaps. {comparison}.")+    lines += [f'<desc id="description">{escape(description)}</desc>',+              f'<text x="30" y="70" font-size="25" font-weight="700" fill="#f8fafc">{label}</text>',+              f'<text x="1008" y="68" text-anchor="end" font-size="15" font-weight="600" fill="{color}">{escape(comparison)}</text>']+    cards = [("PASSING RECORDS", counts["PASS"], "#34d399", "Including inherited suite summaries"),+             ("FAILURES / TIMEOUTS", counts["FAIL"] + counts["TIMEOUT"], "#fb7185", "Driver checks requiring attention"),+             ("UNSUPPORTED APIS", counts["UNSUPPORTED"], "#60a5fa", "Explicit gaps in the driver API"),+             ("ENVIRONMENT GAPS", counts["ENVIRONMENT_UNAVAILABLE"], "#fbbf24", "Fixtures not available on this host")]+    for index, (title, value, color, caption) in enumerate(cards):+        x = 30 + index * 248+        lines += [f'<rect x="{x}" y="94" width="234" height="128" rx="11" fill="#172338" stroke="#334155"/>',+                  f'<rect x="{x}" y="109" width="3" height="32" rx="1" fill="{color}"/>',+                  f'<text x="{x+16}" y="120" font-size="11" font-weight="700" letter-spacing="0.7" fill="#cbd5e1">{title}</text>',+                  f'<text x="{x+16}" y="171" font-size="43" font-weight="700" fill="{color}">{value}</text>',+                  f'<text x="{x+16}" y="201" font-size="10.5" fill="#94a3b8">{caption}</text>']+    provenance = (f"working tree {completed['attempt']['inputs']['sha256'][:12]}"+                  if completed['attempt'].get('uncommitted_review')+                  else f"commit {data['build']['git_commit'][:12]}")+    footer = f"{data['server']['identity'][0]}  /  {data['repetitions']} benchmark repetitions  /  {provenance}"+    lines += [f'<text x="30" y="258" font-size="13" fill="#cbd5e1">{escape(footer)}</text>',+              '<text x="30" y="281" font-size="11" fill="#94a3b8">Counts include two inherited suite summaries. Missing APIs and environment gaps are not passes.</text>',+              '</g>', '</svg>']+    return "\n".join(lines) + "\n"+++def run_details(attempt, data):+    lines = ["## Run details", "", "<details>",+             "<summary>Revision, environment and reproducibility</summary>", "",+             "| Field | Latest attempt |", "| --- | --- |",+             f"| State / exit code | {attempt['state']} / {attempt['exit_code']} |",+             f"| Parent commit (working-tree inputs are identified separately) | [`{attempt['inputs']['commit']}`](https://github.com/aantich/mariadb-haskell/commit/{attempt['inputs']['commit']}) |",+             f"| Branch | `{attempt['inputs']['branch']}` |",+             f"| Started / finished (UTC) | {attempt['started_at']} / {attempt['finished_at']} |",+             f"| Input fingerprint | `{attempt['inputs']['sha256']}` |",+             f"| Local logs and build plan | `{attempt['run_directory']}` (ignored by Git) |"]+    if data:+        machine = data["machine"]+        lines += [f"| Server of completed run | {cell(data['server']['identity'][0])} |",+                  f"| Host | {cell(machine.get('cpu_model', machine['machine']))}; macOS {machine.get('macos', '')} |",+                  f"| Compiler / build tool | GHC {data['build']['ghc']}; Cabal {data['build']['cabal']} |",+                  f"| MariaDB Connector/C | {data.get('connector_c_version', 'not recorded')} |",+                  f"| Complete suite / repetitions | {data['complete_suite']} / {data['repetitions']} |",+                  f"| Driver/workloads stable during execution | {data['source_unchanged_during_run']} |"]+        adapter = data['build'].get('recorder_adapter')+        if adapter:+            lines += [f"| Frozen recorder source | `{adapter['frozen_source_sha256']}` |",+                      f"| Package metadata adapter | `{adapter['adapter_sha256']}` |",+                      f"| Adapted recorder body | `{adapter['adapted_source_sha256']}` |"]+            lines += ["", "The independent package uses a separately fingerprinted metadata adapter. It substitutes only the manifest filename and self-package exclusion in the digest-pinned original recorder; entry-point dispatch is handled by the adapter. Frozen workload bodies, sampling and comparison policy remain unchanged. The original file hash is not presented as the adapter's hash.", ""]+    lines += ["", "Committed runs identify their tested revision. An uncommitted review run identifies its parent revision plus SHA-256 hashes of the actual working-tree inputs; it does not claim the parent commit contains these changes. A following results-only commit does not claim another test run. CI checks that the report, graphic and raw results match the current inputs.", "",+              "Full `python3 validation/check.py` runs refresh this live document, [overview.svg](results/overview.svg) and [current.json](results/current.json), including failed attempts. Commit all three with each tested change. Git history retains previous results.", "", "</details>"]+    return lines+++def render(document, baseline):+    attempt = document["latest_attempt"]+    lines = ["# Live MariaDB validation results", "",+             "Current measured source, compared with the original unchanged-driver baseline. Uncommitted review builds are identified by input fingerprints below.", "",+             "![Validation overview: passing records, failures, unsupported APIs and environment gaps](results/overview.svg)", "",+             "[Correctness](#correctness) · [Benchmarks](#benchmarks) · [Run details](#run-details) · [Raw results](results/current.json) · [Original baseline](baselines/macos-arm64-mariadb-12.3.3/SUMMARY.md)", ""]+    if attempt.get("uncommitted_review"):+        lines += ["> [!IMPORTANT]", "> **Uncommitted review build.** Changes are deliberately uncommitted for Anton’s review. Measurements describe the fingerprinted working tree, not the parent commit and not a published main revision.", ""]+    gate = document.get("stage1_acceptance")+    if (attempt["state"] == "COMPLETED" and gate+            and gate.get("tested_input_sha256") == attempt["inputs"]["sha256"]):+        lines += ["## Implementation acceptance", "",+                  f"Stage 1 targeted failures now passing: **{gate['target_passes']} / {gate['target_count']}**. Original-baseline and last-accepted correctness protection: **{'PASS' if gate['correctness_accepted'] else 'FAIL'}**.", "",+                  f"Timing investigations above 5%: **{len(gate['timing_investigations'])}**; allocation investigations above 5%: **{len(gate['allocation_investigations'])}**. These are investigation signals, not automatically accepted costs.", "",+                  "The frozen baseline retains its original meaning in all comparison tables. The last-accepted reference separately prevents an improvement being lost. [Acceptance evidence](results/stage1-gates.json) · [Confirmation history](results/stage1-confirmation.json) · [Supplementary evidence](results/references/stage1-after-final.json) · [Review notes](../docs/STAGE1_IMPLEMENTATION_NOTES.md).", "",+                  "Confirmation and supplementary files identify their own input fingerprints. Evidence with a different fingerprint is historical and does not confirm the current source.", ""]+        if gate.get('session_target_count'):+            lines += [f"Stage 4 session/reset targets passing: **{gate['session_target_passes']} / {gate['session_target_count']}**. [Session reuse and observations](../docs/STAGE4_IMPLEMENTATION.md).", ""]+        if gate.get('statement_target_count'):+            lines += [f"Stage 3 prepared/native targets passing: **{gate['statement_target_passes']} / {gate['statement_target_count']}**. [Roadmap and continuation](../docs/ROADMAP.md) · [Stage 3 implementation and validation](../docs/STAGE3_IMPLEMENTATION.md).", ""]+        if gate.get('previous_stage_reference'):+            lines += [f"Stage 2 native-connection targets passing: **{gate['native_target_passes']} / {gate['native_target_count']}**. Every PASS from the previous published stage is separately protected. [Stage 2 implementation and validation](../docs/STAGE2_IMPLEMENTATION.md).", ""]+        if gate['performance_requires_confirmation']:+            lines += ["> [!WARNING]","> **Performance acceptance remains open.** Timing/allocation signals require investigation across the entire confirmation window; they have not been waived or accepted automatically.",""]+        if gate.get('evidence_failures'):+            lines += ["> [!CAUTION]","> Measurement evidence rejected: "+cell('; '.join(gate['evidence_failures'])),""]+        if gate['correctness_failures']:+            lines += ["<details><summary>Acceptance failures</summary>", ""] + ["- "+cell(x) for x in gate['correctness_failures']] + ["", "</details>", ""]+    if attempt["inputs"]["dirty_inputs"]:+        lines += ["**Uncommitted test/build inputs:** the tested contents differ from the recorded commit: "+                  + ", ".join(f"`{p}`" for p in attempt["inputs"]["dirty_inputs"]) + ".", ""]+    if attempt["state"] != "COMPLETED":+        lines += ["> [!CAUTION]", f"> **Latest attempt stopped at {attempt['stage']}.** {attempt['reason']}",+                  "> Retained results below are older and do not validate this attempt.", ""]+    completed = document.get("latest_completed_run")+    if not completed:+        return "\n".join(lines + ["No complete suite has been published yet.", ""] + run_details(attempt, None))+    data = completed["results"]+    counts = Counter(c["status"] for c in data["correctness"])+    old_counts = Counter(c["status"] for c in baseline["correctness"])+    comparison = data.get("comparison")+    regressions = comparison["regressions"] if comparison else []+    improvements = comparison["improved_cases"] if comparison else []+    corrected_timeout = ("validation/hs/Validation/Stage7.hs" in completed['attempt']['inputs']['files']+        and any(case['id'] == 'reliability.timeout-before-metadata' and case['status'] == 'PASS'+                for case in data['correctness']))+    if corrected_timeout:+        improvements = [ident for ident in improvements if ident != 'reliability.timeout-before-metadata']+        lines += ["> [!NOTE]", "> **Approved test-contract correction:** `reliability.timeout-before-metadata` now asserts safe retirement, rejection of further commands and a healthy fresh connection after cancellation. Its former reuse expectation contradicted the passing retirement test. The original baseline and failure history remain unchanged. This passing assertion is excluded from the driver-improvement list below. [Contract and evidence](../docs/STAGE7_IMPLEMENTATION.md).", ""]+    failed = counts["FAIL"] + counts["TIMEOUT"]+    if attempt["state"] != "COMPLETED":+        lines += ["## Last completed suite (older results)", "",+                  f"The results below are from `{data['build']['git_commit']}` at {data['recorded_at']}; they do **not** validate the failed attempt above.", ""]+    if failed:+        lines += ["> [!CAUTION]", f"> **{failed} failed or timed-out check records remain.** Missing APIs and environment gaps are tracked separately; they are not passing tests.", ""]+    if comparison:+        marker = "🟠 Attention required" if regressions else "🟢 No new regression signals"+        lines += [f"**Baseline comparison: {marker}** · {len(regressions)} regression {'signal' if len(regressions) == 1 else 'signals'} · {len(improvements)} newly passing {'check' if len(improvements) == 1 else 'checks'}.", "",+                  "A passing comparison does not mean all functionality passes.", ""]+        if regressions:+            lines += ["<details open>", "<summary>🟠 Comparison signals — investigation required</summary>", ""]+            lines += ["- " + cell(p) for p in regressions] + ["", "</details>", ""]+        if improvements:+            lines += ["<details>", "<summary>🟢 Newly passing checks</summary>", ""]+            lines += ["- `" + cell(p) + "`" for p in improvements] + ["", "</details>", ""]+    else:+        lines += ["> [!NOTE]", "> No regression comparison was requested for this run.", ""]+    lines += ["| Reference | Commit |", "| --- | --- |",+              f"| **Baseline** · original unchanged-driver run | `{baseline['build']['git_commit'][:12]}` |",+              f"| **Current** · latest completed run | `{data['build']['git_commit'][:12]}` |", "",+              "Both comparison tables use these same two runs.", "",+              "## Correctness", "",+              "Counts include two aggregate records for the inherited unit/property and integration suites; see [coverage](COVERAGE.md). Change = Current − Baseline, in check records.", "",+              "| Status | Baseline | Current | Change vs baseline |", "| --- | ---: | ---: | ---: |"]+    for status in ("PASS", "FAIL", "TIMEOUT", "UNSUPPORTED", "ENVIRONMENT_UNAVAILABLE"):+        change = counts[status] - old_counts[status]+        delta = f"{change:+d}" if change else "0"+        if change and status in ("PASS", "FAIL", "TIMEOUT"):+            better = change > 0 if status == "PASS" else change < 0+            delta = ("🟢 " if better else "🔴 ") + delta+        lines.append(f"| {status_label(status)} | {old_counts[status]} | **{counts[status]}** | {delta} |")+    for statuses, title in [({"FAIL", "TIMEOUT"}, "🔴 Failed or timed-out checks"),+                            ({"UNSUPPORTED"}, "🔵 Missing driver APIs"),+                            ({"ENVIRONMENT_UNAVAILABLE"}, "🟡 Unavailable environment fixtures")]:+        cases = [c for c in data["correctness"] if c["status"] in statuses]+        if not cases:+            continue+        lines += ["", "<details>", f"<summary>{title} ({len(cases)}) — expand all findings</summary>", "",+                  "| Test | Current status | Detail |", "| --- | --- | --- |"]+        for case in cases:+            detail = case.get("detail", "").strip().splitlines()+            lines.append(f"| `{cell(case['id'])}` | {status_label(case['status'])} | {cell(detail[0] if detail else 'process deadline exceeded')} |")+        lines += ["", "</details>"]+    speed_comparable = compatible(data, baseline)+    lines += ["", "## Benchmarks", "",+              "**Lower time is faster.** Both timing columns show median **ns/unit**; p95 describes complete operations in the Current run. [Coverage](COVERAGE.md) defines each workload's unit.", "",+              "**Change = 100 × (Current − Baseline) / Baseline**, using unrounded medians. 🟢 Negative = faster · 🟡 Positive = slower · 🔴 More than 20% slower = investigate. ⚪ Zero at displayed precision = unchanged.", "",+              "> [!NOTE]", "> A benchmark marked PASS completed its workload. Its timing can still trigger a slowdown alert.", ""]+    if gate and gate.get('statement_target_passes') == 5:+        lines += ["> [!NOTE]", "> Native bulk execution and metadata caching are implemented. The two original native benchmark inventory slots remain frozen historical placeholders; current feature measurements and same-build comparisons are recorded in the [Stage 3 evidence](../docs/STAGE3_IMPLEMENTATION.md).", ""]+    if not speed_comparable:+        lines += ["> [!WARNING]", "> Performance profiles differ or comparability was not established; speed deltas are withheld.", ""]+    else:+        lines += ["Environment, compiler, dependencies and benchmark workloads match the original baseline.", ""]+    native = attempt.get('native_features')+    if native and native.get('complete') and native.get('inputs', {}).get('sha256') == attempt['inputs']['sha256']:+        lines += ["### Native MariaDB features", "",+                  "Same-build alternatives below use identical data, transactions and checksums. These new feature comparisons are separate from the original frozen baseline. Lower time/allocation is better; changes are `(current / baseline - 1) × 100`.", "",+                  "| Comparison | Baseline ns/unit | Current ns/unit | Change vs baseline | Allocation change |",+                  "| --- | ---: | ---: | ---: | ---: |"]+        for name, comparison in native['comparisons'].items():+            old = native['profiles'][comparison['baseline']]['median_elapsed_ns_per_unit']+            new = native['profiles'][comparison['current']]['median_elapsed_ns_per_unit']+            changes = comparison['change_percent']+            lines.append(f"| `{name}` | {old:,.1f} | {new:,.1f} | {changes['elapsed_ns']:+.1f}% | {changes['allocated_bytes']:+.1f}% |")+        lines += ["", "Native timing units are rows for bulk inserts and complete queries for cached/uncached reads. Seven or more independent processes per profile each perform two warm-ups; all samples, CPU/GC counters and checksums are embedded in [current.json](results/current.json).", ""]+        comparison = native.get('baseline_comparison')+        if comparison:+            lines += [f"Native feature baseline comparison: **{'comparable' if comparison['comparable'] else 'different profile; deltas withheld'}**; timing investigations above 5%: **{len(comparison['timing_investigations'])}**; allocation investigations: **{len(comparison['allocation_investigations'])}**. [Immutable native baseline](baselines/macos-arm64-native-statements/results.json).", ""]+    reuse = attempt.get('session_reuse')+    if reuse and reuse.get('complete') and reuse.get('inputs', {}).get('sha256') == attempt['inputs']['sha256']:+        lines += ["### Session reuse", "",+                  "Baseline = reconnect, initialize, prepare, execute and close. Current = reset, restore, prepare and execute the same request on one connection. All application settings and absence of prior-request data are verified; preparation and restoration are inside timing. Changes are `(current / baseline - 1) × 100`; lower is better.", "",+                  "| Transport | Baseline ns/request | Current ns/request | Change vs baseline | Allocation change |",+                  "| --- | ---: | ---: | ---: | ---: |"]+        for name, comparison in reuse['comparisons'].items():+            old = reuse['profiles'][comparison['baseline']]['median_elapsed_ns_per_unit']+            new = reuse['profiles'][comparison['current']]['median_elapsed_ns_per_unit']+            changes = comparison['change_percent']+            lines.append(f"| `{name}` | {old:,.1f} | {new:,.1f} | {changes['elapsed_ns']:+.1f}% | {changes['allocated_bytes']:+.1f}% |")+        snapshot = reuse['profiles']['session.snapshot']+        comparison = reuse['baseline_comparison']+        lines += ["", f"Public snapshot read: **{snapshot['median_elapsed_ns_per_unit']:,.1f} ns/call**, **{snapshot['median_allocated_bytes_per_unit']:,.1f} allocated bytes/call**, no SQL. Seven or more independent processes per profile, with two warmups; raw samples and checksums are embedded in [current.json](results/current.json).", "",+                  f"Immutable reuse baseline comparison: **{'comparable' if comparison['comparable'] else 'different profile; deltas withheld'}**; timing investigations above 5%: **{len(comparison['timing_investigations'])}**; allocation investigations: **{len(comparison['allocation_investigations'])}**. [First reuse baseline](baselines/macos-arm64-session-reuse/results.json).", ""]+    cdc = attempt.get('cdc')+    if cdc and cdc.get('complete') and cdc.get('inputs', {}).get('sha256') == attempt['inputs']['sha256']:+        comparison = cdc['baseline_comparison']+        baseline_cdc = json.loads((HERE / 'baselines/macos-arm64-cdc/results.json').read_text())+        lines += ["### MariaDB CDC", "",+            "[CDC contracts and limits](../docs/STAGE5_IMPLEMENTATION.md) describe transaction checkpoints and explicit acknowledgements. XA-aware checkpointing and storage-engine-binlog GTID semisync remain outside this implementation.", "",+            "Separate immutable first CDC capture; no equivalent CDC throughput measurement exists in the unchanged-driver baseline. Units are rows, except framing/CRC uses events. Live profiles include source connection/setup and checkpoint observation; source log creation is outside timing. Retention timing includes four forced major collections and is a memory workload.", "",+            "| Workload | Baseline ns/unit | Current ns/unit | Change vs baseline | Current allocated B/unit |",+            "| --- | ---: | ---: | ---: | ---: |"]+        for ident, profile in cdc['profiles'].items():+            old = baseline_cdc['profiles'][ident]['median_elapsed_ns_per_unit']+            new = profile['median_elapsed_ns_per_unit']+            delta = comparison['profiles'].get(ident, {}).get('elapsed_ns')+            change = f"{delta:+.1f}%" if delta is not None else 'different profile'+            lines.append(f"| `{ident}` | {old:,.1f} | {new:,.1f} | {change} | {profile['median_allocated_bytes_per_unit']:,.1f} |")+        retained = cdc['profiles']['cdc.retention']['samples']+        last = [sample['retained_heap'][-1][1] for sample in retained]+        lines += ["", f"Streaming retained heap at 1,000,000 rows: **{min(last):,}–{max(last):,} bytes** after major GC across the measured processes. All warmup/measured checkpoints are retained in [current.json](results/current.json). This is live heap, not a process high-water mark.", "",+            f"CDC comparison: **{'comparable' if comparison['comparable'] else 'different profile; deltas withheld'}**; timing investigations above 5%: **{len(comparison['timing_investigations'])}**; allocation investigations: **{len(comparison['allocation_investigations'])}**. [First CDC capture](baselines/macos-arm64-cdc/results.json).", ""]+    wire = attempt.get('wire_features')+    if wire and wire.get('complete') and wire.get('inputs', {}).get('sha256') == attempt['inputs']['sha256']:+        lines += ["### Streamed parameters and optional wire features", "",+            "Same-build alternatives use identical values and checksums. Lower time/allocation is better; change is `(current / baseline - 1) × 100`. Upload units are bytes, cursor/transport units are rows, and progress units are complete SELECT queries. Preparation/connection setup are excluded. Progress measures the negotiated idle-callback path; real progress delivery is tested separately.", "",+            "| Comparison | Baseline ns/unit | Current ns/unit | Change vs baseline | Allocation change |",+            "| --- | ---: | ---: | ---: | ---: |"]+        for name, pair in wire['comparisons'].items():+            old = wire['profiles'][pair['baseline']]['median_elapsed_ns_per_unit']+            new = wire['profiles'][pair['current']]['median_elapsed_ns_per_unit']+            changes = pair['change_percent']+            lines.append(f"| `{name}` | {old:,.2f} | {new:,.2f} | {changes['elapsed_ns']:+.1f}% | {changes['allocated_bytes']:+.1f}% |")+        comparison = wire['baseline_comparison']+        lines += ["", "Server cursors materialize results and add fetch round trips. Compression is optional and trades CPU/allocations for fewer network bytes; local timing is not a WAN result. These features need not outperform ordinary streaming on this Mac.", "",+            f"[First wire-feature capture](baselines/macos-arm64-wire-features/results.json): **{'comparable' if comparison['comparable'] else 'different profile; deltas withheld'}**; timing investigations above 5%: **{len(comparison['timing_investigations'])}**; allocation investigations: **{len(comparison['allocation_investigations'])}**. All seven-repetition samples are retained in [current.json](results/current.json).", ""]+    external_auth = attempt.get('external_authentication')+    if external_auth and external_auth.get('complete'):+        lines += ["### External authentication setup", "",+            "Complete connect/authenticate/session-initialize/query/close costs; units are verified connections. PAM uses TLS and GSSAPI uses a pre-acquired Kerberos service ticket over TCP. KDC ticket acquisition is excluded. The fixture image identity must match before timing deltas are compared.", "",+            "| Profile | Baseline ns/connection | Current ns/connection | Change vs baseline | Allocation change |",+            "| --- | ---: | ---: | ---: | ---: |"]+        from stage7_benchmarks import specification+        import feature_benchmarks+        auth_baseline, _ = feature_benchmarks.load_baseline(specification())+        comparison = external_auth['baseline_comparison']+        for ident, profile in external_auth['profiles'].items():+            original_profile = auth_baseline['profiles'][ident]+            changes = comparison['profiles'].get(ident)+            delta = f"{changes['elapsed_ns']:+.1f}%" if changes else 'withheld: different profile'+            allocation = f"{changes['allocated_bytes']:+.1f}%" if changes else 'withheld'+            lines.append(f"| `{ident}` | {original_profile['median_elapsed_ns_per_unit']:,.2f} | {profile['median_elapsed_ns_per_unit']:,.2f} | {delta} | {allocation} |")+        lines += ["", f"[First authentication capture](baselines/macos-arm64-external-authentication/results.json): timing investigations **{len(comparison['timing_investigations'])}**; allocation investigations **{len(comparison['allocation_investigations'])}**. These authentication alternatives have different security/transport work and are not compared against one another.", ""]+    engines = sorted({ident.partition("/")[0] for ident in data["benchmarks"]}, key=lambda engine: (engine != "haskell", engine))+    for engine in engines:+        title = {"haskell": "Haskell driver", "connector-c": "MariaDB Connector/C control"}.get(engine, engine)+        lines += [f"### {title}", "",+                  "| Workload | Baseline (ns/unit) | Current (ns/unit) | Change vs baseline | Current p95 (ms) | Current status |",+                  "| --- | ---: | ---: | --- | ---: | --- |"]+        for ident, bench in sorted(data["benchmarks"].items()):+            if ident.partition("/")[0] != engine:+                continue+            name = cell(ident.partition("/")[2])+            old = baseline["benchmarks"].get(ident, {})+            before = f"{old['median_ns_per_unit']:.1f}" if old.get("status") == "PASS" else "—"+            if bench["status"] != "PASS":+                lines.append(f"| `{name}` | {before} | — | — | — | {status_label(bench['status'])} |")+                continue+            delta = "—"+            if speed_comparable and before != "—":+                percent = (bench["median_ns_per_unit"] / old["median_ns_per_unit"] - 1) * 100+                if round(percent, 1) == 0:+                    delta = "⚪ +0.0% unchanged"+                elif percent < 0:+                    delta = f"🟢 {percent:+.1f}% faster"+                else:+                    delta = f"{'🔴' if percent > 20 else '🟡'} {percent:+.1f}% slower"+            lines.append(f"| `{name}` | {before} | **{bench['median_ns_per_unit']:.1f}** | {delta} | {bench['p95_operation_ns']/1e6:.3f} | {status_label('PASS')} |")+        lines.append("")+    lines += ["— means no successful measurement is available, or the runs cannot be compared for Change.", "",+              f"Each measured process has two warm-ups; this run has **{data['repetitions']} repetitions** per profile. All samples, allocations, GC statistics, checksums and latency measurements are retained in [current.json](results/current.json).", ""]+    lines += run_details(attempt, data)+    return "\n".join(lines) + "\n"+++def publish(attempt, data=None, *, current=CURRENT, report=REPORT, baseline=BASELINE, gate=None):+    document = json.loads(current.read_text()) if current.exists() else {"schema": 1}+    document["latest_attempt"] = attempt+    if gate is not None:+        document["stage1_acceptance"] = gate+    elif data is not None:+        # A new completed run must not inherit acceptance from an older run,+        # even when a documentation-only rerun has the same source fingerprint.+        document.pop("stage1_acceptance", None)+    if data is not None:+        if not data["complete_suite"] or data["repetitions"] < 7:+            raise ValueError("partial/short runs cannot replace the live full-suite report")+        if data["build"]["git_commit"] != attempt["inputs"]["commit"]:+            raise ValueError("recorder commit differs from the tested commit")+        document["latest_completed_run"] = {"attempt": attempt, "results": data}+    text = render(document, json.loads(baseline.read_text()))+    overview = render_overview(document)+    atomic_write(current, json.dumps(document, indent=2) + "\n")+    atomic_write(report, text)+    atomic_write(current.parent / "overview.svg", overview)+++def verify(document, snapshot, *, allow_working_tree=False):+    attempt = document["latest_attempt"]+    if attempt["state"] != "COMPLETED":+        raise ValueError("latest full validation did not complete")+    completed = document["latest_completed_run"]+    if completed["attempt"] != attempt:+        raise ValueError("live results are from an older attempt")+    data = completed["results"]+    if not data["complete_suite"] or data["repetitions"] < 7 or not data["source_unchanged_during_run"]:+        raise ValueError("live results do not cover a stable complete suite with seven repetitions")+    if attempt["inputs"]["dirty_inputs"] or snapshot["dirty_inputs"]:+        if not (allow_working_tree and attempt.get("uncommitted_review")):+            raise ValueError("test/build inputs are uncommitted; review runs require --working-tree explicitly")+    if attempt["inputs"]["files"] != snapshot["files"]:+        raise ValueError("live results are stale; run python3 validation/check.py on the current code")+    if attempt["inputs"]["sha256"] != snapshot["sha256"]:+        raise ValueError("recorded input fingerprint does not match the current input snapshot")+    if document.get('stage1_acceptance', {}).get('statement_target_count'):+        from stage3_benchmarks import verify_evidence+        verify_evidence(attempt.get('native_features', {}), attempt['inputs'])+    if any(case['id'].startswith('stage4.') for case in data['correctness']):+        from stage4_benchmarks import verify_evidence as verify_reuse+        verify_reuse(attempt.get('session_reuse', {}), attempt['inputs'])+    if any(case['id'].startswith('stage5.') for case in data['correctness']):+        from stage5_benchmarks import verify_evidence as verify_cdc+        verify_cdc(attempt.get('cdc', {}), attempt['inputs'])+    if any(case['id'].startswith('stage6.') for case in data['correctness']):+        from stage6_benchmarks import verify_evidence as verify_wire+        verify_wire(attempt.get('wire_features', {}), attempt['inputs'])+    if any(case['id'].startswith('stage7.') for case in data['correctness']):+        from stage7_benchmarks import verify_evidence as verify_auth+        verify_auth(attempt.get('external_authentication', {}), attempt['inputs'])+    if data["build"]["git_commit"] != attempt["inputs"]["commit"]:+        raise ValueError("recorder and report disagree about the tested commit")+    if 'mariadb-haskell.cabal' in attempt['inputs']['files']:+        from release_recorder import verify_identity+        verify_identity(data['build'], attempt['inputs']['files'])+++def main():+    parser = argparse.ArgumentParser(description=__doc__)+    parser.add_argument("--check", action="store_true", required=True,+                        help="fail if the live report does not cover current committed inputs")+    parser.add_argument("--working-tree", action="store_true", help="verify an explicitly recorded uncommitted review build")+    args = parser.parse_args()+    try:+        document = json.loads(CURRENT.read_text())+        verify(document, input_snapshot(), allow_working_tree=args.working_tree)+        if REPORT.read_text() != render(document, json.loads(BASELINE.read_text())):+            raise ValueError("RESULTS.md does not match current.json")+        if OVERVIEW.read_text() != render_overview(document):+            raise ValueError("overview.svg does not match current.json")+    except (ValueError, KeyError, FileNotFoundError) as error:+        raise SystemExit(str(error))+    print("Live report covers the fingerprinted uncommitted review inputs." if args.working_tree else+          "Live report covers current committed test/build inputs; results-only publication commits are allowed.")+++if __name__ == "__main__":+    main()
+ validation/release_recorder.py view
@@ -0,0 +1,76 @@+#!/usr/bin/env python3+"""Run the frozen benchmark recorder with the fork's package metadata.++``run.py`` is a checksummed historical measurement input. Keep its workloads,+ordering, sampling, comparison policy and bytes unchanged. This adapter admits+exactly two package-identity substitutions and handles the command-line entry+point itself. Both source and adapter hashes are recorded: matching workload+hashes never assert that this metadata adapter existed in the original run.++The pinned source digest makes an unexpected recorder edit a hard failure,+rather than silently deriving a new benchmark from a changed implementation.+"""+import hashlib as _hashlib+from pathlib import Path as _Path++_FROZEN_PATH = _Path(__file__).with_name('run.py')+_FROZEN_SHA256 = 'e756bc0f593b54bc9207fd88d2a53120a4654832650d0a148555a69ebd413346'+_SUBSTITUTIONS = (+    ('ROOT / "mysql-haskell.cabal"', 'ROOT / "mariadb-haskell.cabal"'),+    ('if p["pkg-name"] != "mysql-haskell"', 'if p["pkg-name"] != "mariadb-haskell"'),+)+_ENTRY_POINT = '\nif __name__ == "__main__":\n    raise SystemExit(main())\n'+++def _adapt_source(source):+    """Admit identity changes only; never normalize arbitrary recorder edits."""+    if _hashlib.sha256(source).hexdigest() != _FROZEN_SHA256:+        raise ValueError('frozen recorder changed; package metadata adapter requires review')+    text = source.decode('utf-8')+    for old, new in _SUBSTITUTIONS:+        if text.count(old) != 1:+            raise ValueError('unexpected package metadata site in frozen recorder')+        text = text.replace(old, new)+    if not text.endswith(_ENTRY_POINT):+        raise ValueError('unexpected frozen recorder entry point')+    return text.removesuffix(_ENTRY_POINT)+++_ADAPTED_SOURCE = _adapt_source(_FROZEN_PATH.read_bytes())+_ADAPTED_SHA256 = _hashlib.sha256(_ADAPTED_SOURCE.encode()).hexdigest()+# Execute trusted, pinned repository code in this module's namespace. This+# preserves ordinary function globals and mockability; no workload is copied+# into a second implementation that could drift away from the original.+exec(compile(_ADAPTED_SOURCE, str(_FROZEN_PATH), 'exec'), globals())+_frozen_snapshot = snapshot+++def recorder_identity():+    """Deterministic provenance, independently verifiable during publication."""+    return {+        'frozen_source_sha256': _FROZEN_SHA256,+        'adapter_sha256': _hashlib.sha256(_Path(__file__).read_bytes()).hexdigest(),+        'adapted_source_sha256': _ADAPTED_SHA256,+        'substitutions': [list(pair) for pair in _SUBSTITUTIONS],+        'entry_point': 'validation/release_recorder.py',+    }+++def snapshot():+    """Retain frozen workload identities and record the actual metadata adapter."""+    result = _frozen_snapshot()+    result['recorder_adapter'] = recorder_identity()+    return result+++def verify_identity(build, files):+    """Reject missing/forged adapter provenance in a release publication."""+    expected = recorder_identity()+    if (files.get('validation/run.py') != expected['frozen_source_sha256']+            or files.get('validation/release_recorder.py') != expected['adapter_sha256']+            or build.get('recorder_adapter') != expected):+        raise ValueError('recorder metadata adapter does not match the tested inputs')+++if __name__ == '__main__':+    raise SystemExit(main())
+ validation/run.py view
@@ -0,0 +1,352 @@+#!/usr/bin/env python3+"""Run the actual driver in isolated processes and retain immutable evidence.++No expected-failure wrappers. A broken/unsupported case remains visible.+Server availability is a hard prerequisite, before any test/benchmark starts.+"""+from __future__ import annotations++import argparse+import datetime as dt+import hashlib+import json+import math+import os+from pathlib import Path+import platform+import random+import shlex+import shutil+import statistics+import subprocess+import sys+import tempfile+import time++ROOT = Path(__file__).resolve().parents[1]+HERE = ROOT / "validation"+++def command(args, *, env=None, timeout=60, check=True, input=None):+    return subprocess.run(args, cwd=ROOT, env=env, input=input, capture_output=True,+                          text=True, timeout=timeout, check=check)+++def executable(target):+    return command(["cabal", "list-bin", "-O2", "--enable-tests", target]).stdout.strip().splitlines()[-1]+++def digest_files(paths):+    return {str(p.relative_to(ROOT)): hashlib.sha256(p.read_bytes()).hexdigest()+            for p in sorted(paths) if p.is_file()}+++def snapshot():+    cabal = (ROOT / "mysql-haskell.cabal").read_text()+    library = cabal[cabal.index("library\n"):cabal.index("test-suite test\n")]+    return {+        "git_commit": command(["git", "rev-parse", "HEAD"]).stdout.strip(),+        "git_branch": command(["git", "branch", "--show-current"]).stdout.strip(),+        "driver_source_files": digest_files((ROOT / "src").rglob("*.hs")),+        "library_cabal_sha256": hashlib.sha256(library.encode()).hexdigest(),+        "workload_files": digest_files([*HERE.rglob("*.hs"), HERE / "connector_c.c", HERE / "run.py"]),+        "ghc": command(["ghc", "--numeric-version"]).stdout.strip(),+        "cabal": command(["cabal", "--numeric-version"]).stdout.strip(),+    }+++def percentile(values, p):+    ordered = sorted(values)+    if len(ordered) == 1:+        return ordered[0]+    position = (len(ordered) - 1) * p+    lo = math.floor(position)+    return ordered[lo] + (ordered[math.ceil(position)] - ordered[lo]) * (position - lo)+++def cli_path():+    override = os.environ.get("MB_CLIENT")+    if override:+        return override+    found = shutil.which("mariadb")+    if found:+        return found+    path = Path("/opt/homebrew/opt/mariadb@12.3/bin/mariadb")+    if path.exists():+        return str(path)+    raise RuntimeError("mariadb client not found; install/start the managed server first")+++def sql_client(env, query, client_certificate=False):+    # Keep the generated development password out of argv, logs and artifacts.+    with tempfile.TemporaryDirectory(prefix="mariadb-validation-client-") as folder:+        cnf = Path(folder) / "client.cnf"+        options=[+            "user=" + env["MB_USER"], "password=" + env["MB_PASSWORD"],+            "host=" + env["MB_HOST"], "port=" + env["MB_PORT"],+            "protocol=tcp", "ssl=" + ("1" if client_certificate else "0")]+        if client_certificate:+            options += ["ssl-verify-server-cert=1", "ssl-ca="+env["MB_TLS_CA"],+                        "ssl-cert="+env["MB_TLS_CLIENT_CERT"], "ssl-key="+env["MB_TLS_CLIENT_KEY"]]+        cnf.write_text("[client]\n" + "\n".join(options) + "\n")+        cnf.chmod(0o600)+        return command([cli_path(), "--defaults-extra-file=" + str(cnf),+                        "--batch", "--skip-column-names", env["MB_DATABASE"]],+                       input=query, timeout=60).stdout.strip()+++def server_metadata(env):+    query = "SELECT VERSION(), @@version_comment, @@version_compile_machine;"+    query += " SHOW VARIABLES WHERE Variable_name IN ('port','bind_address','character_set_server','collation_server','max_allowed_packet','binlog_format','binlog_row_image','binlog_checksum','log_bin','innodb_flush_log_at_trx_commit','sync_binlog','innodb_buffer_pool_size','sql_mode','lower_case_table_names','have_ssl','max_connections','time_zone');"+    lines = sql_client(env, query).splitlines()+    return {"identity": lines[0].split("\t"), "variables": dict(line.split("\t", 1) for line in lines[1:])}+++def case_result(binary, ident, area, env, log, timeout=20):+    start = time.monotonic()+    try:+        result = command([binary, "case", ident], env=env, timeout=timeout, check=False)+        status = {0: "PASS", 3: "UNSUPPORTED", 4: "ENVIRONMENT_UNAVAILABLE"}.get(result.returncode, "FAIL")+        stdout, stderr = result.stdout, result.stderr+        code = result.returncode+    except subprocess.TimeoutExpired as exc:+        status, code = "TIMEOUT", None+        stdout = exc.stdout or ""+        stderr = exc.stderr or ""+        if isinstance(stdout, bytes): stdout = stdout.decode(errors="replace")+        if isinstance(stderr, bytes): stderr = stderr.decode(errors="replace")+    log.write_text(stdout + stderr)+    return {"id": ident, "area": area, "status": status, "returncode": code,+            "seconds": time.monotonic() - start, "detail": (stderr or stdout)[-4000:]}+++def compile_control():+    config = os.environ.get("MB_CONFIG") or shutil.which("mariadb_config")+    if not config:+        config = "/opt/homebrew/opt/mariadb@12.3/bin/mariadb_config"+    flags = shlex.split(command([config, "--include", "--libs"]).stdout)+    binary = HERE / ".local" / "connector-c"+    binary.parent.mkdir(exist_ok=True)+    command([os.environ.get("CC", "cc"), "-O2", str(HERE / "connector_c.c"),+             *flags, "-o", str(binary)])+    return str(binary), command([str(binary), "--version"]).stdout.strip()+++def benchmark_sample(binary, ident, iterations, env, log):+    args = [binary, "bench", ident, str(iterations), "+RTS", "-N4", "-T", "-RTS"] if Path(binary).name != "connector-c" else [binary, ident, str(iterations)]+    try:+        result = command(args, env=env, timeout=180, check=False)+    except subprocess.TimeoutExpired:+        log.write_text("benchmark process exceeded 180 seconds\n")+        return {"status": "TIMEOUT", "detail": "benchmark process deadline exceeded"}+    log.write_text(result.stdout + result.stderr)+    if result.returncode:+        return {"status": "UNSUPPORTED" if result.returncode == 3 else "FAIL",+                "detail": (result.stderr or result.stdout)[-2000:]}+    line = next((line for line in result.stdout.splitlines() if line.startswith("BENCH\t")), None)+    if not line:+        return {"status": "FAIL", "detail": "missing benchmark record"}+    fields = line.split("\t")+    names = ["units", "elapsed_ns", "allocated_bytes", "gc_cpu_ns", "max_live_bytes", "cpu_ns", "checksum"]+    sample = {name: (int(value) if int(value) >= 0 else None) for name, value in zip(names, fields[2:9])}+    sample["operation_latencies_ns"] = [int(x) for x in fields[9].split(",")]+    sample["ns_per_unit"] = sample["elapsed_ns"] / sample["units"]+    sample["status"] = "PASS"+    return sample+++def compare(current, baseline, performance):+    problems, improvements = [], []+    old = {case["id"]: case for case in baseline["correctness"]}+    new = {case["id"]: case for case in current["correctness"]}+    for ident, previous in old.items():+        latest = new.get(ident)+        if latest is None:+            problems.append("test removed: " + ident)+        elif previous["status"] == "PASS" and latest["status"] != "PASS":+            problems.append(f"{ident}: PASS -> {latest['status']}")+        elif previous["status"] != "PASS" and latest["status"] == "PASS":+            improvements.append(ident)+        elif previous["status"] in ("FAIL", "TIMEOUT") and latest["status"] in ("UNSUPPORTED", "ENVIRONMENT_UNAVAILABLE"):+            problems.append(f"{ident}: failure reclassified as {latest['status']}")+    for ident, latest in new.items():+        if ident not in old and latest["status"] in ("FAIL","TIMEOUT","ENVIRONMENT_UNAVAILABLE"):+            problems.append("new failing/unavailable test: " + ident)+    # A changed workload is a new experiment, never a direct speed comparison.+    same_workloads = all(current["build"]["workload_files"].get(path) == value+                         for path,value in baseline["build"]["workload_files"].items()+                         if path.endswith(("Validation/Benchmark.hs", "Validation/Common.hs", "connector_c.c", "run.py")))+    if performance:+        same_machine = current["machine"] == baseline["machine"]+        same_server = current["server"] == baseline["server"]+        same_build = all(current["build"][key] == baseline["build"][key] for key in ("ghc", "cabal"))+        same_dependencies = current["dependency_plan_sha256"] == baseline["dependency_plan_sha256"]+        if not (same_workloads and same_machine and same_server and same_build and same_dependencies):+            problems.append("performance profile differs: same workload, machine, server settings, compiler and dependency plan are required")+        else:+            for ident, previous in baseline["benchmarks"].items():+                latest = current["benchmarks"].get(ident)+                if previous["status"] != "PASS": continue+                if not latest or latest["status"] != "PASS":+                    problems.append("benchmark no longer completes: " + ident)+                elif latest["median_ns_per_unit"] > previous["median_ns_per_unit"] * 1.20:+                    problems.append(f"{ident}: median slowed by more than 20%; confirm on an idle machine")+    return {"regressions": problems, "improved_cases": improvements,+            "same_workloads": same_workloads, "performance_checked": performance}+++def report(data):+    counts = {}+    for case in data["correctness"]: counts[case["status"]] = counts.get(case["status"], 0) + 1+    lines = ["# MariaDB driver baseline", "", "This run tests the current driver without implementation fixes.", "",+             f"Server: **{data['server']['identity'][0]}**. Driver commit: `{data['build']['git_commit'][:12]}`.", "",+             " | ".join(f"{key}: **{value}**" for key, value in counts.items()), "",+             "## Correctness findings", "", "| Test | Status | Detail |", "| --- | --- | --- |"]+    for case in data["correctness"]:+        if case["status"] != "PASS":+            detail = case["detail"].strip().splitlines()[0] if case["detail"].strip() else "process deadline exceeded"+            lines.append(f"| {case['id']} | {case['status']} | {detail.replace('|', '/')} |")+    lines += ["", "## Benchmarks", "", "Two warm-up operations precede every measured process. Seven repetitions are the default. Raw samples and operation latencies are retained in JSON. A query/transaction/decoder batch is the latency unit; ns/unit normalizes by rows, bytes or decoder rows as specified in COVERAGE.md.", "",+              "| Workload | Status | Median ns/unit | p95 operation ms |", "| --- | --- | ---: | ---: |"]+    for ident, bench in data["benchmarks"].items():+        if bench["status"] == "PASS":+            lines.append(f"| {ident} | PASS | {bench['median_ns_per_unit']:.1f} | {bench['p95_operation_ns']/1e6:.3f} |")+        else: lines.append(f"| {ident} | {bench['status']} | | |")+    if data.get("comparison"):+        lines += ["", "## Comparison", "", json.dumps(data["comparison"], indent=2)]+    return "\n".join(lines) + "\n"+++def main():+    parser = argparse.ArgumentParser(description=__doc__)+    parser.add_argument("--config", type=Path, default=HERE / ".local" / "environment.json")+    parser.add_argument("--output", type=Path)+    parser.add_argument("--repetitions", type=int, default=7)+    parser.add_argument("--only", help="substring filter for harness development; cannot freeze a partial run")+    parser.add_argument("--no-upstream", action="store_true")+    parser.add_argument("--no-benchmarks", action="store_true")+    parser.add_argument("--compare", type=Path)+    parser.add_argument("--correctness-only-comparison", action="store_true")+    parser.add_argument("--freeze", type=Path, help="new immutable baseline directory; must not exist")+    args = parser.parse_args()+    if args.repetitions < 1: parser.error("repetitions must be positive")+    if args.freeze and (args.only or args.no_upstream or args.no_benchmarks or args.repetitions < 7):+        parser.error("a frozen baseline requires the complete suite and at least seven repetitions")+    if args.freeze and args.freeze.exists(): parser.error("baseline already exists; refusing to overwrite")+    env = os.environ.copy()+    env.update(json.loads(args.config.read_text()))+    env["TZ"] = "UTC"+    binary = executable("exe:mariadb-validation")+    # Hard dependency gate: there is no synthetic fallback for a missing server.+    preflight = command([binary, "preflight"], env=env, timeout=15, check=False)+    if preflight.returncode:+        raise SystemExit("MariaDB preflight failed; no tests or benchmarks started:\n" + preflight.stdout + preflight.stderr)+    print(preflight.stdout.strip(), flush=True)+    output = args.output or HERE / "results" / "runs" / dt.datetime.now(dt.timezone.utc).strftime("%Y%m%dT%H%M%SZ")+    output.mkdir(parents=True, exist_ok=False)+    logs = output / "logs"; logs.mkdir()+    build = snapshot()+    reference="92a94012bc553a672c73d409dad1d17e9db1e53f"+    unchanged=command(["git","diff","--quiet",reference,"--","src"],check=False).returncode==0+    original_cabal=command(["git","show",reference+":mysql-haskell.cabal"]).stdout+    original_library=original_cabal[original_cabal.index("library\n"):original_cabal.index("test-suite test\n")]+    library_unchanged=hashlib.sha256(original_library.encode()).hexdigest()==build["library_cabal_sha256"]+    if args.freeze and not (unchanged and library_unchanged):+        raise SystemExit("initial baseline requires unchanged driver source and library configuration")+    build.update(reference_driver_commit=reference,driver_matches_reference=unchanged,library_configuration_matches_reference=library_unchanged)+    plan = ROOT / "dist-newstyle/cache/plan.json"+    # Compare resolved dependencies, not absolute compiler/store paths or targets.+    plan_data = json.loads(plan.read_text())+    dependencies = sorted({(p["pkg-name"],p["pkg-version"]) for p in plan_data["install-plan"] if p["pkg-name"] != "mysql-haskell"})+    data = {"schema": 1, "recorded_at": dt.datetime.now(dt.timezone.utc).isoformat(),+            "complete_suite": not (args.only or args.no_upstream or args.no_benchmarks),+            "build": build, "server": server_metadata(env), "dependencies": dependencies,+            "dependency_plan_sha256": hashlib.sha256(json.dumps(dependencies).encode()).hexdigest(),+            "machine": {"system": platform.system(), "release": platform.release(), "machine": platform.machine(),+                        "macos": platform.mac_ver()[0], "cpu_count": os.cpu_count(),+                        "cpu_model": command(["sysctl","-n","machdep.cpu.brand_string"],check=False).stdout.strip() if sys.platform == "darwin" else platform.processor()},+            "correctness": [], "benchmarks": {}, "repetitions": args.repetitions,+            "haskell_benchmark_rts": ["-N4", "-T"], "process_timezone":"UTC"}+    data["fixture_controls"]={}+    for plugin,user in [("native","mariadb_haskell_test"),("ed25519","mariadb_ed25519"),+                        ("parsec","mariadb_parsec"),("sha2","mariadb_sha2"),("mutual_tls","mariadb_x509")]:+        control_env={**env,"MB_USER":user}+        value=sql_client(control_env,"SELECT 937",client_certificate=(plugin=="mutual_tls"))+        if value!="937": raise RuntimeError("invalid authentication fixture: "+plugin)+        data["fixture_controls"][plugin]="PASS"+    listed = command([binary, "list"]).stdout.splitlines()+    ids = [line.split("\t")[0] for line in listed]+    if len(set(ids)) != len(ids): raise RuntimeError("duplicate test IDs")+    for line in listed:+        ident, area = line.split("\t")+        if args.only and args.only not in ident: continue+        result = case_result(binary, ident, area, env, logs / (ident + ".log"), 45 if area == "large-packets" else 20)+        data["correctness"].append(result)+        (output / "partial-results.json").write_text(json.dumps(data,indent=2) + "\n")+        print(result["status"], ident, flush=True)+    if not args.no_upstream and not args.only:+        for target, name, extra, timeout in [("test:test", "inherited.unit", ["--quickcheck-replay=730208","--quickcheck-tests=100"], 300),+                                             ("test:integration", "inherited.integration", ["--num-threads=1","--timeout=180s"], 300)]:+            testenv = env.copy(); testenv["MYSQL_TLS_CA_PATH"] = env["MB_TLS_CA"]; testenv["MYSQL_UNIX_SOCKET"] = env["MB_SOCKET"]+            started = time.monotonic()+            try:+                result = command([executable(target), *extra], env=testenv, timeout=timeout, check=False)+                status = "PASS" if result.returncode == 0 else "FAIL"+                full_log = result.stdout + result.stderr+                detail = full_log[-12000:]+            except subprocess.TimeoutExpired:+                status, detail = "TIMEOUT", "inherited test process exceeded its deadline"+                full_log = detail+            (logs / (name + ".log")).write_text(full_log)+            data["correctness"].append({"id": name,"area":"inherited","status":status,"seconds":time.monotonic()-started,"detail":detail})+            print(status, name, flush=True)+    if not args.no_benchmarks and not args.only:+        sql_client(env, "DROP TABLE IF EXISTS mb_bench_data; CREATE TABLE mb_bench_data(id BIGINT PRIMARY KEY,n BIGINT NOT NULL,payload VARCHAR(128) NOT NULL) ENGINE=InnoDB; INSERT INTO mb_bench_data SELECT seq,seq*3,RPAD('x',128,'x') FROM seq_1_to_10000;")+        control, version = compile_control()+        data["connector_c_version"] = version+        controls = {"latency.text","read.text.narrow","read.text.wide","write.text","write.execute-many","blob.text"}+        workloads = [line.split("\t") for line in command([binary,"list-benchmarks"]).stdout.splitlines()]+        schedule = [("haskell", ident, int(n), binary) for ident,n in workloads]+        schedule += [("connector-c", ident, int(n), control) for ident,n in workloads if ident in controls]+        for repetition in range(args.repetitions):+            random.Random(730208 + repetition).shuffle(schedule)+            for engine, ident, n, program in schedule:+                key = engine + "/" + ident+                sample = benchmark_sample(program, ident, n, env, logs / f"bench.{engine}.{ident}.{repetition}.log")+                data["benchmarks"].setdefault(key,{"samples":[]})["samples"].append(sample)+                (output / "partial-results.json").write_text(json.dumps(data,indent=2) + "\n")+                print("BENCH", repetition+1, key, sample["status"], flush=True)+        for bench in data["benchmarks"].values():+            samples = bench["samples"]+            bench["status"] = "PASS" if all(s["status"] == "PASS" for s in samples) else next(s["status"] for s in samples if s["status"] != "PASS")+            if bench["status"] == "PASS":+                values = [s["ns_per_unit"] for s in samples]+                latencies = [ns for s in samples for ns in s["operation_latencies_ns"]]+                bench.update(median_ns_per_unit=statistics.median(values),+                             min_ns_per_unit=min(values),max_ns_per_unit=max(values),+                             p50_operation_ns=percentile(latencies,.50),p95_operation_ns=percentile(latencies,.95),p99_operation_ns=percentile(latencies,.99))+    final = snapshot()+    if final["driver_source_files"] != build["driver_source_files"]:+        raise RuntimeError("driver source changed during baseline run")+    if final["workload_files"] != build["workload_files"]:+        raise RuntimeError("test/benchmark workloads changed during run")+    data["source_unchanged_during_run"] = True+    if args.compare:+        data["comparison"] = compare(data,json.loads(args.compare.read_text()),not args.correctness_only_comparison)+    (output / "results.json").write_text(json.dumps(data,indent=2) + "\n")+    (output / "SUMMARY.md").write_text(report(data))+    shutil.copy2(plan, output / "build-plan.json")+    if args.freeze:+        args.freeze.mkdir(parents=True)+        for name in ("results.json","SUMMARY.md"): shutil.copy2(output/name,args.freeze/name)+        (args.freeze / "SHA256SUMS").write_text("".join(hashlib.sha256((args.freeze/name).read_bytes()).hexdigest()+"  "+name+"\n" for name in ("results.json","SUMMARY.md")))+        print("Frozen baseline:",args.freeze)+    print("Results:",output / "SUMMARY.md")+    if args.compare and data["comparison"]["regressions"]: return 1+    # Initial capture deliberately returns nonzero when correctness fails.+    if not args.compare and any(c["status"] in ("FAIL","TIMEOUT","ENVIRONMENT_UNAVAILABLE") for c in data["correctness"]): return 1+    return 0+++if __name__ == "__main__":+    raise SystemExit(main())
+ validation/stage1_gates.py view
@@ -0,0 +1,261 @@+#!/usr/bin/env python3+"""Additive acceptance policy. Never rewrites frozen workloads or measured values."""+from __future__ import annotations+import argparse+import hashlib+import json+import statistics+from pathlib import Path++HERE=Path(__file__).resolve().parent+TARGETS=[+'protocol.continuation-sequence-rejected','protocol.large-row-is-not-eof','protocol.empty-packet-predicate',+'packets.live-blob-16777216','packets.live-blob-16777217','packets.live-blob-33554430',+'protocol.column-definition-roundtrip','protocol.reset-does-not-consume-pending-row',+'reliability.reset-unread-result','reliability.server-kill-query','reliability.tls-cancellation-propagates',+'transaction.partial-result-exception','transaction.nested-preserves-outer',+'protocol.unsigned-last-insert-id','protocol.text-field-trailing-data','protocol.text-field-integer-overflow',+'sql.unsigned-generated-id','sql.zero-date-preservation','connection.default-full-unicode','types.non-utf8-results',+'parameters.literal-question-mark','parameters.comment-question-mark','parameters.no-backslash-text']++# Native-connection work is independent of the deferred CDC/prepared-output work.+NATIVE_TARGETS = [+    'protocol.binary-plugin-challenge', 'protocol.unknown-plugin-rejected',+    'protocol.native-extension-handshake',+    *['auth.' + plugin + '.' + transport for plugin in ['ed25519', 'parsec']+      for transport in ['tcp', 'tls', 'socket']],+    'auth.sha2.tcp', 'auth.sha2.socket', 'auth.mutual-tls-helper-client',+    'connection.localhost-address-fallback', 'connection.mozilla-ca-store-installed',+]++STATEMENT_TARGETS = [+    'protocol.prepared-out-capability', 'sql.procedure-prepared-out',+    'capability.binary-bulk', 'capability.metadata-cache', 'capability.extended-metadata',+]++SESSION_TARGETS = ['capability.session-tracking', 'capability.reset-connection']+CDC_TARGETS = ['cdc.multiple-table-maps', 'cdc.repeated-row-events', 'cdc.table-id-mismatch',+               'cdc.crc32-corruption-rejected', *['cdc.native-event-'+str(n) for n in range(160,172)],+               'cdc.gtid-resume', 'cdc.durable-ack-control']++def results(document):+    """Accept either a portable raw run or the live publication envelope."""+    if "latest_completed_run" in document:+        return document["latest_completed_run"]["results"]+    return document+++def reference(path=HERE / "results/accepted.json"):+    """Load the separately approved reference, never the candidate itself."""+    manifest = json.loads(path.read_text())+    root = path.parent.resolve()+    target = (root / manifest["reference"]).resolve()+    if not target.is_relative_to(root):+        raise ValueError("accepted reference escapes results directory")+    if target == root / "current.json":+        raise ValueError("live current results cannot be their own accepted reference")+    raw = target.read_bytes()+    if hashlib.sha256(raw).hexdigest() != manifest["sha256"]:+        raise ValueError("accepted reference hash mismatch")+    accepted = results(json.loads(raw))+    if (not manifest.get("tested_commit")+            or accepted.get("build", {}).get("git_commit") != manifest["tested_commit"]):+        raise ValueError("accepted reference and manifest disagree about the tested commit")+    return accepted, manifest+++def median_allocation(benchmark):+    """Normalize raw Haskell samples; absent C heap metrics stay unavailable."""+    samples = [+        sample["allocated_bytes"] / sample["units"]+        for sample in benchmark.get("samples", [])+        if sample.get("allocated_bytes") is not None and sample.get("units", 0) > 0+    ]+    return statistics.median(samples) if samples else None+++def compare(candidate, previous, original, *, require_targets=True, previous_stage=None):+    """Keep correctness acceptance separate from measurement/investigation gates.++    Known original failures remain visible. New failures cannot be added to that+    deferred set, and an accepted improvement cannot be lost just because its+    original-baseline status was FAIL. Timing signals require human resolution;+    they are never silently treated as accepted costs.+    """+    failures, timing, allocation, evidence = [], [], [], []+    if (not candidate.get("complete_suite")+            or candidate.get("repetitions", 0) < 7+            or not candidate.get("source_unchanged_during_run")):+        evidence.append("candidate is not a stable complete seven-repetition run")+    new = {case["id"]: case for case in candidate["correctness"]}+    if len(new) != len(candidate["correctness"]):+        failures.append("duplicate correctness IDs")+    references = [("original", original), ("accepted", previous)]+    if previous_stage is not None:+        references.append(("previous_stage", previous_stage))+    for label, old in references:+        for case in old["correctness"]:+            latest = new.get(case["id"])+            if latest is None:+                failures.append(f"{label}: removed {case['id']}")+            elif case["status"] == "PASS" and latest["status"] != "PASS":+                failures.append(f"{label}: lost PASS {case['id']} -> {latest['status']}")+            elif (case["status"] in ("FAIL", "TIMEOUT")+                  and latest["status"] in ("UNSUPPORTED", "ENVIRONMENT_UNAVAILABLE")):+                failures.append(f"{label}: hidden failure {case['id']}")+    original_ids = {case["id"] for case in original["correctness"]}+    deferred_ids = {case["id"] for case in original["correctness"]+                    if case["status"] in ("FAIL", "TIMEOUT")}+    statuses = {"PASS", "FAIL", "TIMEOUT", "UNSUPPORTED", "ENVIRONMENT_UNAVAILABLE"}+    for ident, case in new.items():+        if case["status"] not in statuses:+            failures.append("unknown correctness status: " + ident)+        # Reusing an inventory ID is not permission to extend the known failure+        # set. A newly implemented/available feature must pass its real case.+        if case["status"] in ("FAIL", "TIMEOUT") and ident not in deferred_ids:+            failures.append("failure outside original deferred set: " + ident)+        if ident not in original_ids and case["status"] != "PASS":+            failures.append("new case not passing: " + ident)+    if any(ident.startswith('stage7.') for ident in new):+        # Stage 7 protects every published Stage 6 pass independently of the+        # unchanged performance references. The approved timeout assertion+        # correction is reported separately from driver improvements.+        try:+            published, _ = reference(HERE / 'results/stage7-reference.json')+            for case in published['correctness']:+                if case['status'] == 'PASS' and new.get(case['id'], {}).get('status') != 'PASS':+                    failures.append('Stage 6: lost PASS ' + case['id'])+        except (OSError, ValueError, KeyError) as error:+            evidence.append('Stage 6 correctness reference invalid: ' + str(error))+        for ident in ['auth.pam-environment', 'auth.gssapi-environment', 'reliability.timeout-before-metadata']:+            if new.get(ident, {}).get('status') != 'PASS':+                failures.append('required external-auth/timeout target not passing: ' + ident)+    if require_targets:+        for ident in TARGETS:+            if new.get(ident, {}).get("status") != "PASS":+                failures.append("required target not passing: " + ident)++    if require_targets and previous_stage is not None:+        for ident in NATIVE_TARGETS:+            if new.get(ident, {}).get("status") != "PASS":+                failures.append("required native target not passing: " + ident)++    if require_targets and previous_stage is not None:+        for ident in STATEMENT_TARGETS:+            if new.get(ident, {}).get("status") != "PASS":+                failures.append("required statement target not passing: " + ident)++    if require_targets and previous_stage is not None:+        for ident in SESSION_TARGETS:+            if new.get(ident, {}).get("status") != "PASS":+                failures.append("required session target not passing: " + ident)++    if require_targets and previous_stage is not None:+        for ident in CDC_TARGETS:+            if new.get(ident, {}).get('status') != 'PASS':+                failures.append('required CDC target not passing: '+ident)++    if require_targets and any(ident.startswith('stage6.') for ident in new):+        for ident in ('capability.long-data', 'capability.cursor-fetch',+                      'capability.progress-reporting', 'capability.compression'):+            if new.get(ident, {}).get('status') != 'PASS':+                failures.append('required wire feature not passing: ' + ident)++    for ident, benchmark in candidate.get("benchmarks", {}).items():+        prior_status = original.get("benchmarks", {}).get(ident, {}).get("status")+        if benchmark["status"] not in statuses:+            failures.append("unknown benchmark status: " + ident)+        if benchmark["status"] in ("FAIL", "TIMEOUT") and prior_status not in ("FAIL", "TIMEOUT"):+            failures.append("new failed benchmark: " + ident)++    for label, old in references:+        comparable = profile_matches(candidate, old)+        if old.get("benchmarks") and not comparable:+            evidence.append(label + ": performance profile differs; no speed/allocation comparison is valid")+        for ident, benchmark in old.get("benchmarks", {}).items():+            current = candidate.get("benchmarks", {}).get(ident)+            if current is None:+                failures.append(f"{label}: removed benchmark {ident}")+                continue+            if benchmark["status"] != "PASS":+                continue+            if current["status"] != "PASS":+                failures.append(f"{label}: missing/failed benchmark {ident}")+                continue+            if not comparable:+                continue+            before, after = benchmark["median_ns_per_unit"], current["median_ns_per_unit"]+            if after > before * 1.05:+                timing.append({"reference": label, "id": ident, "percent": 100 * (after / before - 1)})+            before_bytes, after_bytes = median_allocation(benchmark), median_allocation(current)+            if before_bytes and after_bytes and after_bytes > before_bytes * 1.05:+                allocation.append({"reference": label, "id": ident,+                                   "percent": 100 * (after_bytes / before_bytes - 1)})+    return {+        "schema": 1,+        "correctness_failures": failures,+        "evidence_failures": evidence,+        "evidence_accepted": not evidence,+        "timing_investigations": timing,+        "allocation_investigations": allocation,+        "target_passes": sum(new.get(ident, {}).get("status") == "PASS" for ident in TARGETS),+        "target_count": len(TARGETS),+        "native_target_passes": sum(new.get(ident, {}).get("status") == "PASS" for ident in NATIVE_TARGETS),+        "native_target_count": len(NATIVE_TARGETS),+        "statement_target_passes": sum(new.get(ident, {}).get("status") == "PASS" for ident in STATEMENT_TARGETS),+        "statement_target_count": len(STATEMENT_TARGETS),+        "session_target_passes": sum(new.get(ident, {}).get("status") == "PASS" for ident in SESSION_TARGETS),+        "session_target_count": len(SESSION_TARGETS),+        "cdc_target_passes": sum(new.get(ident, {}).get("status") == "PASS" for ident in CDC_TARGETS),+        "cdc_target_count": len(CDC_TARGETS),+        "correctness_accepted": not failures,+        "performance_requires_confirmation": bool(timing or allocation),+    }+++def profile_matches(candidate, reference):+    """Refuse performance claims across different environments or workloads."""+    for key in ("machine", "server", "dependency_plan_sha256", "connector_c_version"):+        if key not in candidate or candidate[key] != reference.get(key):+            return False+    for key in ("ghc", "cabal"):+        if (key not in candidate.get("build", {})+                or candidate["build"][key] != reference.get("build", {}).get(key)):+            return False+    files = reference.get("build", {}).get("workload_files", {})+    frozen = [+        (path, digest) for path, digest in files.items()+        if path.endswith(("Validation/Benchmark.hs", "Validation/Common.hs", "connector_c.c", "run.py"))+    ]+    return len(frozen) == 4 and all(+        candidate.get("build", {}).get("workload_files", {}).get(path) == digest+        for path, digest in frozen+    )+++def main():+    parser = argparse.ArgumentParser(description=__doc__)+    parser.add_argument("candidate", type=Path)+    parser.add_argument("--output", type=Path, default=HERE / "results/stage1-gates.json")+    args = parser.parse_args()+    accepted, manifest = reference()+    original = json.loads((HERE / "baselines/macos-arm64-mariadb-12.3.3/results.json").read_text())+    document = json.loads(args.candidate.read_text())+    data = results(document)+    if "latest_attempt" in document:+        import publish_results as live+        live.verify(document, live.input_snapshot(),+                    allow_working_tree=document["latest_attempt"].get("uncommitted_review", False))+    previous_stage, stage_manifest = reference(HERE / "results/previous-stage.json")+    report = compare(data, accepted, original, previous_stage=previous_stage)+    report["previous_stage_reference"] = stage_manifest+    report["accepted_reference"] = manifest+    report["tested_input_sha256"] = document.get("latest_attempt", {}).get("inputs", {}).get("sha256")+    args.output.write_text(json.dumps(report, indent=2) + "\n")+    print(json.dumps(report, indent=2))+    return 0 if (report["correctness_accepted"] and report["evidence_accepted"]+                 and not report["performance_requires_confirmation"]) else 1+++if __name__ == "__main__":+    raise SystemExit(main())
+ validation/stage3_benchmarks.py view
@@ -0,0 +1,65 @@+#!/usr/bin/env python3+"""Native statement workloads; the shared recorder preserves all evidence guards."""+from pathlib import Path+import release_recorder as run+import feature_benchmarks as features++PROFILES = {+    'bulk.native': 5, 'bulk.units': 5, 'bulk.prepared': 5, 'bulk.text-batch': 5,+    'prepared.narrow.cached': 500, 'prepared.narrow.uncached': 500,+    'prepared.wide.cached': 500, 'prepared.wide.uncached': 500,+}+PAIRS = {+    'native_vs_sequential': ('bulk.native', 'bulk.prepared'),+    'native_vs_text_batch': ('bulk.native', 'bulk.text-batch'),+    'unit_vs_aggregate': ('bulk.units', 'bulk.native'),+    'cached_vs_uncached_narrow': ('prepared.narrow.cached', 'prepared.narrow.uncached'),+    'cached_vs_uncached_wide': ('prepared.wide.cached', 'prepared.wide.uncached'),+}+BASELINE_DIRECTORY = Path('baselines/macos-arm64-native-statements')+# Warm-ups and the measured regions live in these Haskell sources. Python+# evidence-policy fixes must not disable comparisons with an unchanged workload.+# The sample checks below separately enforce each profile's units/iterations.+WORKLOAD_PATHS = ('validation/hs/Validation/Stage3Bench.hs', 'validation/hs/Validation/Common.hs')+++def specification():+    return features.FeatureSpec(+        marker='STAGE3', command='stage3-bench', label='native',+        profiles=PROFILES,+        units={ident: count * (1000 if ident.startswith('bulk.') else 1) for ident,count in PROFILES.items()},+        checksums={ident: count * (629500 if ident.startswith('bulk.') else 3847 if '.wide.' in ident else 7)+                   for ident,count in PROFILES.items()},+        pairs=PAIRS, baseline_directory=BASELINE_DIRECTORY, workload_paths=WORKLOAD_PATHS)+++def sample(output, ident):+    return features.sample(specification(), output, ident)+++def verify_measurements(data, inputs):+    return features.verify_measurements(specification(), data, inputs)+++def load_native_baseline():+    return features.load_baseline(specification())+++def native_baseline_comparison(data):+    return features.baseline_comparison(specification(), data)+++def compare_native_baseline(data):+    return features.attach_baseline_comparison(specification(), data)+++def verify_evidence(data, inputs):+    return features.verify_evidence(specification(), data, inputs)+++def main():+    return features.main(specification())+++if __name__ == '__main__':+    main()
+ validation/stage4_benchmarks.py view
@@ -0,0 +1,30 @@+#!/usr/bin/env python3+"""Complete reset/restore/prepare requests versus reconnects, plus snapshot cost."""+from pathlib import Path+import feature_benchmarks as features++PROFILES = {**{f'reuse.{transport}.{mode}': 30+              for transport in ('tcp', 'tls', 'socket') for mode in ('reset', 'reconnect')},+            'session.snapshot': 100000}+PAIRS = {transport: (f'reuse.{transport}.reset', f'reuse.{transport}.reconnect')+         for transport in ('tcp', 'tls', 'socket')}+BASELINE_DIRECTORY = Path('baselines/macos-arm64-session-reuse')+WORKLOAD_PATHS = ('validation/hs/Validation/Stage4Bench.hs', 'validation/hs/Validation/Common.hs')+++def specification():+    return features.FeatureSpec(marker='STAGE4', command='stage4-bench', label='session reuse',+        profiles=PROFILES, units=PROFILES, checksums=PROFILES, pairs=PAIRS,+        baseline_directory=BASELINE_DIRECTORY, workload_paths=WORKLOAD_PATHS)+++def verify_evidence(data, inputs):+    return features.verify_evidence(specification(), data, inputs)+++def main():+    return features.main(specification())+++if __name__ == '__main__':+    main()
+ validation/stage5_benchmarks.py view
@@ -0,0 +1,27 @@+#!/usr/bin/env python3+"""CDC decoding, checked framing, live delivery and retained streaming heap."""+from pathlib import Path+import feature_benchmarks as features++PROFILES = {'cdc.decode.single-table': 10000, 'cdc.decode.interleaved': 10000,+            'cdc.decode.sparse-update': 10000, 'cdc.decode.compressed': 2000, 'cdc.framing.crc32': 20000,+            'cdc.live.rows': 1000, 'cdc.live.checkpoints': 100, 'cdc.retention': 1000000}+BASELINE_DIRECTORY = Path('baselines/macos-arm64-cdc')+WORKLOAD_PATHS = ('validation/hs/Validation/Stage5Bench.hs', 'validation/hs/Validation/Common.hs')+++def specification():+    return features.FeatureSpec(marker='STAGE5', command='stage5-bench', label='CDC',+        profiles=PROFILES, units=PROFILES,+        checksums={name: n*(n+1)//2 if name.startswith('cdc.decode.') or name=='cdc.retention' else n+                   for name,n in PROFILES.items()}, pairs={},+        baseline_directory=BASELINE_DIRECTORY, workload_paths=WORKLOAD_PATHS,+        heap_profiles={'cdc.retention': (1000,10000,100000,1000000)})+++def verify_evidence(data, inputs):+    features.verify_evidence(specification(), data, inputs)+++if __name__ == '__main__':+    features.main(specification())
+ validation/stage6_benchmarks.py view
@@ -0,0 +1,65 @@+#!/usr/bin/env python3+"""Streamed parameters, cursor batches and optional wire feature costs."""+from pathlib import Path+import sys+import feature_benchmarks as features+import release_recorder as run++PROFILES = {'upload.buffered': 4, 'upload.streamed': 4, 'upload.retention': 67108864,+            'cursor.stream': 1, 'cursor.batch32': 1, 'cursor.batch512': 1, 'cursor.batch4096': 1,+            'transport.narrow.plain': 2, 'transport.narrow.zlib': 2,+            'transport.wide.plain': 2, 'transport.wide.zlib': 2,+            'progress.disabled': 500, 'progress.enabled': 500}+BASELINE_DIRECTORY = Path('baselines/macos-arm64-wire-features')+WORKLOAD_PATHS = ('validation/hs/Validation/Stage6Bench.hs', 'validation/hs/Validation/Common.hs')+++def specification():+    units, checksums = {}, {}+    for name, count in PROFILES.items():+        if name == 'upload.retention':+            units[name] = checksums[name] = count+        elif name.startswith('upload.'):+            units[name] = checksums[name] = count * 1048576+        elif name.startswith('cursor.'):+            units[name], checksums[name] = count * 10000, count * 51285000+        elif name.startswith('transport.'):+            width = 2048 if '.wide.' in name else 32+            units[name], checksums[name] = count * 1000, count * (500500 + 1000 * width)+        else:+            units[name], checksums[name] = count, count * 937+    return features.FeatureSpec(marker='STAGE6', command='stage6-bench', label='Wire features',+        profiles=PROFILES, units=units, checksums=checksums,+        pairs={'streamed upload': ('upload.streamed', 'upload.buffered'),+               **{f'cursor batch {size}': (f'cursor.batch{size}', 'cursor.stream') for size in (32, 512, 4096)},+               **{f'{width} compression': (f'transport.{width}.zlib', f'transport.{width}.plain') for width in ('narrow','wide')},+               'progress negotiation': ('progress.enabled', 'progress.disabled')},+        baseline_directory=BASELINE_DIRECTORY, workload_paths=WORKLOAD_PATHS,+        heap_profiles={'upload.retention': (65536, 1048576, 16777216, 67108864)})+++def verify_evidence(data, inputs):+    features.verify_evidence(specification(), data, inputs)+++def verify_buffering_control(binary):+    """The real retained-heap guard must reject a whole-source consumer."""+    result = run.command([binary, 'stage6-heap-control', '+RTS', '-N4', '-T', '-RTS'], timeout=30)+    entries = [list(map(int, line.split('\t')[1:])) for line in result.stdout.splitlines()+               if line.startswith('STAGE6_HEAP\t')]+    try:+        features.verify_heap(specification(), 'upload.retention', entries)+    except ValueError as error:+        if 'grows beyond' not in str(error):+            raise+    else:+        raise ValueError('whole-source buffering escaped the retained-heap guard')+    print('Whole-source buffering rejected by the retained-heap guard; all checkpoints retained.')+    print(result.stdout, end='')+++if __name__ == '__main__':+    if len(sys.argv) == 3 and sys.argv[1] == '--verify-heap-control':+        verify_buffering_control(sys.argv[2])+    else:+        features.main(specification())
+ validation/stage7_benchmarks.py view
@@ -0,0 +1,70 @@+#!/usr/bin/env python3+"""Independent PAM/Kerberos establishment costs; existing workloads stay frozen."""+from pathlib import Path+import hashlib+import json+import re+import feature_benchmarks as features+import auth_fixture+import release_recorder as run++PROFILES = {'auth.pam.password': 10, 'auth.pam.multiple': 10,+            'auth.gssapi.cached-ticket': 10}+WORKLOAD_PATHS = ('validation/hs/Validation/Stage7Bench.hs',+                  'validation/hs/Validation/ExternalAuthFixture.hs',+                  'validation/hs/Validation/Common.hs',+                  'validation/auth_fixture/Dockerfile',+                  'validation/auth_fixture/serve.py',+                  'validation/auth_fixture/pam_fixture.c')+++def legacy_runtime_aliases():+    """Bind the exact first provenance index to its verified runtime only.++    The frozen measurements are untouched. Both build logs identify the same+    configuration blob; its cryptographic digest and complete filesystem/config+    prove this historical equivalence. All other unknown indexes stay distinct.+    """+    folder = run.HERE/'results/experiments'+    proof = json.loads((folder/'stage7-runtime-equivalence.json').read_text())+    raw = (folder/'stage7-authentication-image-config.json').read_bytes()+    expected = 'sha256:'+hashlib.sha256(raw).hexdigest()+    if expected != proof['first_and_verified_runtime_config_digest']:+        raise ValueError('authentication runtime configuration digest mismatch')+    logs = [('stage7-first-authentication.log', 'first_build_log_sha256'),+            ('stage7-runtime-identity-build.log', 'verification_build_log_sha256')]+    for filename, key in logs:+        content = (folder/filename).read_bytes()+        if hashlib.sha256(content).hexdigest() != proof[key]:+            raise ValueError('authentication runtime proof log mismatch')+        if re.findall(r'exporting config (sha256:[0-9a-f]{64})', content.decode()) != [expected]:+            raise ValueError('authentication build did not use the verified configuration')+    original = 'sha256:065d10313899e7a007821c2c052b87608b26a82629e4db273b72987e3cddd12f'+    if proof['first_capture_index'] != original or original not in (folder/logs[0][0]).read_text():+        raise ValueError('authentication initial provenance index mismatch')+    blob = json.loads(raw)+    image = {'Architecture': blob['architecture'], 'Os': blob['os'], 'Variant': blob.get('variant'),+             'RootFS': {'Type': blob['rootfs']['type'], 'Layers': blob['rootfs']['diff_ids']},+             'Config': blob['config']}+    identity = auth_fixture.runtime_identity(image)+    if identity != proof['runtime_identity'] or identity != auth_fixture.runtime_identity(proof['runtime_fields']):+        raise ValueError('authentication runtime fingerprint mismatch')+    return {'MB_AUTH_FIXTURE_IMAGE_ID': {original: identity}}+++def specification():+    return features.FeatureSpec(marker='STAGE7', command='stage7-bench',+        label='External authentication', profiles=PROFILES, units=PROFILES,+        checksums=PROFILES, pairs={},+        baseline_directory=Path('baselines/macos-arm64-external-authentication'),+        workload_paths=WORKLOAD_PATHS,+        environment_profile=('MB_AUTH_FIXTURE_IMAGE_ID',),+        environment_profile_aliases=legacy_runtime_aliases())+++def verify_evidence(data, inputs):+    features.verify_evidence(specification(), data, inputs)+++if __name__ == '__main__':+    features.main(specification())
+ validation/supplementary.py view
@@ -0,0 +1,109 @@+#!/usr/bin/env python3+"""Separate Stage 1 experiments; never change frozen workload/recorder semantics.++Retention experiments collect current live heap after GC. Ordinary timing+profiles retain the original supplementary timing regions and high-water metric.+"""+import argparse+import json+import os+from pathlib import Path+import statistics++import publish_results as live+import release_recorder as run++PROFILES = {+    "connect.tcp": 30,+    "parameters.select": 500,+    "parameters.mixed": 500,+    "transaction.empty": 300,+    "batch.1000": 5,+    "stream.retention": 100000,+}+RETENTION = {+    "retention.10000": 10000,+    "retention.1000000": 1000000,+    "retention.tiny-slice": 16777216,+}+++def read_sample(output, *, retention):+    """Parse the harness protocol, keeping live heap distinct from high-water."""+    marker = "RETENTION" if retention else "STAGE1"+    line = next(line for line in output.splitlines() if line.startswith(marker + "\t"))+    _, _, *values = line.split("\t")+    fields = (["units", "elapsed_ns", "current_live_bytes", "checksum"] if retention+              else ["units", "elapsed_ns", "allocated_bytes", "max_live_bytes", "checksum"])+    if len(fields) != len(values):+        raise ValueError("unexpected supplementary sample shape")+    sample = dict(zip(fields, map(int, values)))+    if retention:+        sample["live_heap_samples_bytes"] = [+            int(line.split("\t")[2]) for line in output.splitlines()+            if line.startswith("LIVE_HEAP\t")+        ]+    return sample+++def main():+    parser = argparse.ArgumentParser(description=__doc__)+    parser.add_argument("--output", type=Path, required=True)+    parser.add_argument("--repetitions", type=int, default=7)+    args = parser.parse_args()+    if args.repetitions < 1:+        parser.error("repetitions must be positive")+    if args.output.exists():+        parser.error("output exists; preserve previous experiment")+    env = os.environ.copy()+    env.update(json.loads((run.HERE / ".local/environment.json").read_text()))+    env["TZ"] = "UTC"+    print("MariaDB:", run.sql_client(env, "SELECT VERSION()"), flush=True)+    inputs = live.input_snapshot()+    # A failed fresh build raises here: an older executable is never measured.+    run.command(["cabal", "build", "exe:mariadb-validation", "-O2", "--enable-tests"], timeout=300)+    if live.input_snapshot()["files"] != inputs["files"]:+        raise RuntimeError("inputs changed during supplementary build")+    binary = run.executable("exe:mariadb-validation")+    data = {"schema": 1, "recorded_at": live.now(), "inputs": inputs,+            "server": run.server_metadata(env), "profiles": {}}+    for ident, count in {**PROFILES, **RETENTION}.items():+        retention = ident in RETENTION+        samples = []+        for repetition in range(args.repetitions):+            output = run.command(+                [binary, "stage1-bench", ident, str(count), "+RTS", "-N4", "-T", "-RTS"],+                env=env, timeout=180,+            ).stdout+            samples.append(read_sample(output, retention=retention))+        if retention:+            # GC checkpoints belong only to these memory experiments. Their+            # elapsed time is not comparable with frozen throughput profiles.+            live_values = [sample["current_live_bytes"] for sample in samples]+            checkpoints = [value for sample in samples for value in sample["live_heap_samples_bytes"]]+            profile = {+                "samples": samples,+                "kind": "retention experiment; GC checkpoints are inside this experiment only",+                "median_current_live_bytes": statistics.median(live_values),+                "maximum_checkpoint_live_bytes": max(live_values + checkpoints),+            }+        else:+            profile = {+                "samples": samples,+                "median_ns_per_unit": statistics.median(+                    sample["elapsed_ns"] / sample["units"] for sample in samples+                ),+                "median_allocated_bytes_per_unit": statistics.median(+                    sample["allocated_bytes"] / sample["units"] for sample in samples+                ),+            }+        data["profiles"][ident] = profile+        print(ident, {key: value for key, value in profile.items() if key != "samples"}, flush=True)+    if live.input_snapshot()["files"] != inputs["files"]:+        raise RuntimeError("inputs changed during supplementary measurement")+    args.output.parent.mkdir(parents=True, exist_ok=True)+    args.output.write_text(json.dumps(data, indent=2) + "\n")+++if __name__ == "__main__":+    main()
+ validation/test_check.py view
@@ -0,0 +1,263 @@+"""A broken acceptance reference must not leave an older run looking current."""+import json+from pathlib import Path+import tempfile+import unittest+from unittest.mock import patch++import check+++class CheckFailureTests(unittest.TestCase):+    def test_build_launch_failure_is_recorded(self):+        self.check_process_failure("build", FileNotFoundError("cabal missing"), "build")++    def test_recorder_launch_failure_is_recorded(self):+        self.check_process_failure("recorder", OSError("cannot start recorder"), "suite")++    def test_unreadable_recorder_result_is_recorded(self):+        self.check_process_failure("json", None, "suite")++    def test_interruption_is_recorded(self):+        self.check_process_failure("build", KeyboardInterrupt(), "build", expected_status=130)++    def check_process_failure(self, failure_at, error, stage, *, expected_status=1):+        with tempfile.TemporaryDirectory() as folder:+            root = Path(folder)+            output = root / "run"+            config = root / "config.json"+            config.write_text("{}")+            inputs = {"sha256": "candidate", "files": {}, "commit": "parent"}++            def recorder(*args, **kwargs):+                if failure_at == "recorder":+                    raise error+                output.mkdir()+                (output / "results.json").write_text("{")+                return 0++            with (patch("sys.argv", ["check.py", "--working-tree", "--output", str(output),+                                     "--config", str(config)]),+                  patch.object(check.live, "input_snapshot", return_value=inputs),+                  patch.object(check.run, "sql_client", return_value="12.3.3-MariaDB"),+                  patch.object(check.subprocess, "run") as build,+                  patch.object(check.subprocess, "call", side_effect=recorder),+                  patch.object(check, "install_runtime_data", return_value=0) as install,+                  patch.object(check.live, "publish") as publish):+                if failure_at == "install":+                    install.side_effect = error+                if failure_at == "build":+                    build.side_effect = error+                else:+                    build.return_value.returncode = 0+                self.assertEqual(expected_status, check.main())+            attempt, data = publish.call_args.args+            self.assertEqual("FAILED", attempt["state"])+            self.assertEqual(stage, attempt["stage"])+            self.assertIsNone(data)++    def test_runtime_data_installation_failure_is_recorded(self):+        self.check_process_failure("install", OSError("copy failed"), "runtime data installation")++    def test_runtime_data_installation_interruption_is_recorded(self):+        self.check_process_failure("install", KeyboardInterrupt(), "runtime data installation", expected_status=130)++    def test_install_selects_library_not_executable_setup(self):+        with tempfile.TemporaryDirectory() as folder:+            package = Path(folder)/"mysql-haskell-1.3.3"+            library = package/"opt"+            component = package/"x/mariadb-validation/opt"+            for build in [library, component]:+                build.mkdir(parents=True)+                (build/"setup-config").touch()+            module = library/"build/Database/MySQL/Connection.hi"+            module.parent.mkdir(parents=True)+            module.touch()+            binary = component/"build/mariadb-validation/mariadb-validation"+            with (patch.object(check.run, "executable", return_value=str(binary)),+                  patch.object(check.subprocess, "run") as copy):+                copy.return_value.returncode=0+                self.assertEqual(0, check.install_runtime_data())+                self.assertIn("--builddir="+str(library), copy.call_args.args[0])++    def test_missing_library_install_configuration_is_rejected(self):+        with patch.object(check.run, "executable", return_value="/no/library/executable"):+            with self.assertRaisesRegex(RuntimeError, "Setup configuration"):+                check.install_runtime_data()++    def test_bad_reference_publishes_failed_attempt_and_retains_raw_candidate(self):+        self.check_reference_failure(ValueError("hash mismatch"))++    def test_absent_manifest_cannot_disable_the_ratchet(self):+        self.check_reference_failure(FileNotFoundError("accepted.json absent"), missing=True)++    def check_reference_failure(self, error, *, missing=False):+        original_exists = Path.exists++        def exists(path):+            return False if missing and path.name == "accepted.json" else original_exists(path)++        with tempfile.TemporaryDirectory() as folder:+            root = Path(folder)+            output = root / "run"+            config = root / "config.json"+            config.write_text("{}")+            inputs = {"sha256": "candidate", "files": {}, "commit": "parent"}++            def recorder(*args, **kwargs):+                output.mkdir()+                (output / "results.json").write_text(json.dumps({"complete_suite": True}))+                return 0++            with (patch("sys.argv", ["check.py", "--working-tree", "--output", str(output),+                                     "--config", str(config)]),+                  patch.object(check.live, "input_snapshot", return_value=inputs),+                  patch.object(check.run, "sql_client", return_value="12.3.3-MariaDB"),+                  patch.object(check.subprocess, "run") as build,+                  patch.object(check.subprocess, "call", side_effect=recorder),+                  patch.object(check.stage1_gates, "reference", side_effect=error) as reference,+                  patch.object(Path, "exists", exists),+                  patch.object(check, "install_runtime_data", return_value=0) as install,+                  patch.object(check.live, "publish") as publish):+                build.return_value.returncode = 0+                self.assertEqual(1, check.main())+            reference.assert_called_once()+            attempt, data = publish.call_args.args+            self.assertEqual("FAILED", attempt["state"])+            self.assertEqual("acceptance evidence", attempt["stage"])+            self.assertIn(str(error), attempt["reason"])+            self.assertIsNone(data)+            self.assertTrue((output / "results.json").exists())+++class NativeWrapperTests(unittest.TestCase):+    def test_native_failure_is_published(self):+        self.check_native(RuntimeError('native failed'), 1)++    def test_native_interruption_is_published(self):+        self.check_native(KeyboardInterrupt(), 130)++    def test_native_success_is_attached_to_completed_run(self):+        self.check_native(None, 0)++    def test_native_original_alarm_retains_complete_results(self):+        self.check_native(None, 1, native_timing=[{'id': 'bulk.native', 'percent': 21}])++    def test_native_investigation_is_not_automatically_accepted(self):+        self.check_native(None, 0, native_timing=[{'id': 'bulk.native', 'percent': 6}])++    def check_native(self, failure, status, native_timing=None):+        with tempfile.TemporaryDirectory() as folder:+            root = Path(folder)+            (root / 'results').mkdir()+            output = root / 'run'+            config = Path(folder) / 'config.json'+            config.write_text('{}')+            inputs = {'sha256': 'candidate', 'files': {}, 'commit': 'parent'}+            candidate = {'complete_suite': True}+            gate = {'correctness_accepted': True, 'evidence_accepted': True}+            native_data = {'complete': True, 'inputs': inputs}+            if native_timing:+                native_data['baseline_comparison'] = {'timing_investigations': native_timing}++            def recorder(*args, **kwargs):+                output.mkdir()+                (output / 'results.json').write_text(json.dumps(candidate))+                return 0++            with (patch('sys.argv', ['check.py', '--working-tree', '--config', str(config), '--output', str(output)]),+                  patch.object(check.live, 'input_snapshot', return_value=inputs),+                  patch.object(check.run, 'HERE', root),+                  patch.object(check.run, 'sql_client', return_value='12.3.3-MariaDB'),+                  patch.object(check.subprocess, 'run') as build,+                  patch.object(check.subprocess, 'call', side_effect=recorder),+                  patch.object(check, 'install_runtime_data', return_value=0),+                  patch.object(check.stage1_gates, 'reference', return_value=({}, {})),+                  patch.object(check.stage1_gates, 'compare', return_value=gate),+                  patch.object(check, 'native_benchmarks', return_value=native_data, side_effect=failure) as native,+                  patch('stage3_benchmarks.verify_evidence'),+                  patch.object(check.live, 'publish') as publish):+                build.return_value.returncode = 0+                self.assertEqual(status, check.main())+            native.assert_called_once_with(output.resolve(), 7, config)+            attempt, data = publish.call_args.args+            if failure is None:+                self.assertEqual('COMPLETED', attempt['state'])+                self.assertEqual(native_data, attempt['native_features'])+                self.assertEqual(candidate, data)+                if native_timing:+                    self.assertTrue(gate['performance_requires_confirmation'])+            else:+                self.assertEqual('FAILED', attempt['state'])+                self.assertEqual('native benchmarks', attempt['stage'])+                self.assertIsNone(data)+                self.assertTrue((output / 'results.json').exists())+++class SessionReuseWrapperTests(unittest.TestCase):+    def test_success_is_attached(self):+        self.check_reuse(None, 0)++    def test_failure_retains_raw_candidate(self):+        self.check_reuse(RuntimeError('reuse failed'), 1)++    def test_interruption_is_recorded(self):+        self.check_reuse(KeyboardInterrupt(), 130)++    def test_large_timing_alarm_keeps_complete_measurements(self):+        self.check_reuse(None, 1, timing=[{'id': 'reuse.tcp.reset', 'percent': 21}])++    def test_strict_investigation_remains_open(self):+        self.check_reuse(None, 0, timing=[{'id': 'reuse.tcp.reset', 'percent': 6}])++    def check_reuse(self, failure, status, timing=None):+        with tempfile.TemporaryDirectory() as folder:+            root = Path(folder)+            (root / 'results').mkdir()+            output = root / 'run'+            config = root / 'config.json'+            config.write_text('{}')+            inputs = {'sha256': 'candidate', 'files': {}, 'commit': 'parent'}+            candidate = {'complete_suite': True, 'correctness': [{'id': 'stage4.fixture', 'status': 'PASS'}]}+            gate = {'correctness_accepted': True, 'evidence_accepted': True}+            reuse_data = {'complete': True, 'inputs': inputs,+                          'baseline_comparison': {'timing_investigations': timing or [], 'allocation_investigations': []}}++            def recorder(*args, **kwargs):+                output.mkdir()+                (output / 'results.json').write_text(json.dumps(candidate))+                return 0++            with (patch('sys.argv', ['check.py', '--working-tree', '--config', str(config), '--output', str(output)]),+                  patch.object(check.live, 'input_snapshot', return_value=inputs),+                  patch.object(check.run, 'HERE', root),+                  patch.object(check.run, 'sql_client', return_value='12.3.3-MariaDB'),+                  patch.object(check.subprocess, 'run') as build,+                  patch.object(check.subprocess, 'call', side_effect=recorder),+                  patch.object(check, 'install_runtime_data', return_value=0),+                  patch.object(check.stage1_gates, 'reference', return_value=({}, {})),+                  patch.object(check.stage1_gates, 'compare', return_value=gate),+                  patch.object(check, 'native_benchmarks', return_value={'complete': True}),+                  patch('stage3_benchmarks.verify_evidence'),+                  patch('stage4_benchmarks.verify_evidence'),+                  patch.object(check, 'session_reuse_benchmarks', return_value=reuse_data, side_effect=failure) as reuse_call,+                  patch.object(check.live, 'publish') as publish):+                build.return_value.returncode = 0+                self.assertEqual(status, check.main())+            reuse_call.assert_called_once_with(output.resolve(), 7, config)+            attempt, data = publish.call_args.args+            if failure is None:+                self.assertEqual('COMPLETED', attempt['state'])+                self.assertEqual(candidate, data)+                self.assertEqual(reuse_data, attempt['session_reuse'])+                if timing:+                    self.assertTrue(gate['performance_requires_confirmation'])+            else:+                self.assertEqual('FAILED', attempt['state'])+                self.assertEqual('session reuse benchmarks', attempt['stage'])+                self.assertIsNone(data)+                self.assertTrue((output / 'results.json').is_file())+++if __name__ == "__main__":+    unittest.main()
+ validation/test_publication.py view
@@ -0,0 +1,181 @@+#!/usr/bin/env python3+"""Protect live-report provenance and keep failed attempts visible."""+import copy+import json+from pathlib import Path+import subprocess+import tempfile+import unittest++import publish_results as live+++class PublicationTests(unittest.TestCase):+    def setUp(self):+        self.folder = tempfile.TemporaryDirectory()+        self.addCleanup(self.folder.cleanup)+        self.root = Path(self.folder.name)+        self.current = self.root / "current.json"+        self.report = self.root / "RESULTS.md"+        self.data = json.loads(live.BASELINE.read_text())+        self.data["comparison"] = {"regressions": [], "improved_cases": [],+                                   "same_workloads": True, "performance_checked": True}+        self.inputs = {"commit": self.data["build"]["git_commit"], "branch": "main",+                       "files": {"src/Driver.hs": "abc"}, "dirty_inputs": [], "sha256": "digest"}+        self.attempt = {"state": "COMPLETED", "stage": "complete", "exit_code": 0,+                        "started_at": "start", "finished_at": "finish", "reason": "",+                        "inputs": self.inputs, "run_directory": "validation/results/runs/example"}++    def publish(self, attempt=None, data=None):+        live.publish(attempt or self.attempt, self.data if data is None else data,+                     current=self.current, report=self.report)+        return json.loads(self.current.read_text())++    def test_native_stage_cannot_publish_without_native_measurements(self):+        document = self.publish()+        document['stage1_acceptance'] = {'statement_target_count': 5}+        with self.assertRaisesRegex(ValueError, 'native benchmark'):+            live.verify(document, self.inputs)++    def test_session_stage_cannot_publish_without_session_measurements(self):+        self.data['correctness'].append({'id': 'stage4.required', 'status': 'PASS'})+        with self.assertRaisesRegex(ValueError, 'session reuse benchmark'):+            live.verify(self.publish(), self.inputs)++    def test_complete_run_includes_failures_and_performance_deltas(self):+        document = self.publish()+        live.verify(document, self.inputs)+        text = self.report.read_text()+        self.assertIn("56 failed or timed-out check records", text)+        self.assertIn("+0.0%", text)+        self.assertIn(self.inputs["commit"], text)+        self.assertIn("does not mean all functionality passes", text)++    def test_review_graphic_identifies_actual_working_tree(self):+        self.attempt["uncommitted_review"] = True+        document = self.publish()+        graphic = live.render_overview(document)+        self.assertIn("working tree digest", graphic)+        self.assertNotIn(" /  commit ", graphic)++    def test_new_completed_run_does_not_inherit_old_acceptance(self):+        document = self.publish()+        document["stage1_acceptance"] = {"tested_input_sha256": "old"}+        self.current.write_text(json.dumps(document))+        self.assertNotIn("stage1_acceptance", self.publish())++    def test_changed_connector_c_version_withholds_performance_comparison(self):+        baseline = copy.deepcopy(self.data)+        self.data["connector_c_version"] = "different"+        self.assertFalse(live.compatible(self.data, baseline))++    def test_results_only_publication_commit_is_allowed(self):+        snapshot = {**self.inputs, "commit": "later-results-only-commit"}+        live.verify(self.publish(), snapshot)++    def test_changed_input_makes_report_stale(self):+        snapshot = {**self.inputs, "files": {"src/Driver.hs": "changed"}}+        with self.assertRaisesRegex(ValueError, "stale"):+            live.verify(self.publish(), snapshot)++    def test_fingerprint_must_match_the_actual_input_snapshot(self):+        document = self.publish()+        snapshot = {**self.inputs, 'sha256': 'different-fingerprint'}+        with self.assertRaisesRegex(ValueError, 'fingerprint'):+            live.verify(document, snapshot)++    def test_dirty_source_cannot_claim_committed_results(self):+        snapshot = {**self.inputs, "dirty_inputs": ["src/Driver.hs"]}+        with self.assertRaisesRegex(ValueError, "committed"):+            live.verify(self.publish(), snapshot)++    def test_dirty_review_requires_both_recorded_intent_and_explicit_check(self):+        self.inputs['dirty_inputs']=['src/Driver.hs']+        self.attempt['uncommitted_review']=True+        document=self.publish()+        with self.assertRaisesRegex(ValueError,'uncommitted'):+            live.verify(document,self.inputs)+        live.verify(document,self.inputs,allow_working_tree=True)+        document['latest_attempt']['uncommitted_review']=False+        document['latest_completed_run']['attempt']['uncommitted_review']=False+        with self.assertRaisesRegex(ValueError,'uncommitted'):+            live.verify(document,self.inputs,allow_working_tree=True)++    def test_review_permission_does_not_accept_stale_source(self):+        self.inputs['dirty_inputs']=['src/Driver.hs']+        self.attempt['uncommitted_review']=True+        snapshot={**self.inputs,'files':{'src/Driver.hs':'different'}}+        with self.assertRaisesRegex(ValueError,'stale'):+            live.verify(self.publish(),snapshot,allow_working_tree=True)++    def test_partial_or_short_runs_cannot_replace_current_results(self):+        self.publish()+        overview = self.current.parent / "overview.svg"+        before = self.current.read_bytes(), self.report.read_bytes(), overview.read_bytes()+        for key, value in [("complete_suite", False), ("repetitions", 1)]:+            data = {**self.data, key: value}+            with self.assertRaisesRegex(ValueError, "partial/short"):+                self.publish(data=data)+            self.assertEqual(before, (self.current.read_bytes(), self.report.read_bytes(), overview.read_bytes()))++    def test_failed_attempt_retains_but_labels_older_complete_results(self):+        before = self.publish()+        failed = {**self.attempt, "state": "FAILED", "stage": "build", "exit_code": 1,+                  "reason": "build failed", "inputs": {**self.inputs, "commit": "new-commit"}}+        live.publish(failed, current=self.current, report=self.report)+        after = json.loads(self.current.read_text())+        self.assertEqual(before["latest_completed_run"], after["latest_completed_run"])+        self.assertIn("Last completed suite (older results)", self.report.read_text())+        self.assertIn("PREVIOUS COMPLETE RUN", (self.current.parent / "overview.svg").read_text())+        with self.assertRaisesRegex(ValueError, "did not complete"):+            live.verify(after, self.inputs)++    def test_recorder_commit_mismatch_is_rejected(self):+        data = copy.deepcopy(self.data)+        data["build"]["git_commit"] = "different"+        with self.assertRaisesRegex(ValueError, "commit differs"):+            self.publish(data=data)++    def test_different_environment_withholds_speed_deltas(self):+        data = copy.deepcopy(self.data)+        data["machine"]["cpu_model"] = "different"+        self.publish(data=data)+        self.assertIn("speed deltas are withheld", self.report.read_text())+        self.assertNotIn("+0.0%", self.report.read_text())++    def test_snapshot_detects_added_changed_and_deleted_source_but_ignores_report(self):+        def git(*args):+            subprocess.run(["git", *args], cwd=self.root, check=True,+                           stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)+        git("init")+        git("config", "user.name", "Test")+        git("config", "user.email", "test@example.invalid")+        (self.root / "src").mkdir()+        source = self.root / "src/Driver.hs"+        source.write_text("original")+        git("add", "src")+        git("commit", "-m", "test")+        initial = live.input_snapshot(self.root)+        self.report.write_text("report-only change")+        self.assertEqual(initial["files"], live.input_snapshot(self.root)["files"])+        source.write_text("changed")+        self.assertNotEqual(initial["files"], live.input_snapshot(self.root)["files"])+        self.assertEqual(live.input_snapshot(self.root)["dirty_inputs"], ["src/Driver.hs"])+        source.unlink()+        self.assertIsNone(live.input_snapshot(self.root)["files"]["src/Driver.hs"])+        (self.root / "src/New.hs").write_text("new")+        self.assertIn("src/New.hs", live.input_snapshot(self.root)["dirty_inputs"])+++if __name__ == "__main__":+    unittest.main()+++class AdditionalBuildInputTests(unittest.TestCase):+    def test_linux_and_ci_configuration_are_fingerprinted(self):+        for path in ['.dockerignore', 'validation/linux/Dockerfile', '.github/workflows/mariadb-linux.yml',+                     'validation/results/accepted.json', 'validation/results/previous-stage.json',+                     'validation/baselines/macos-arm64-native-statements/results.json',+                     'validation/baselines/macos-arm64-native-statements/SUMMARY.md',+                     'validation/baselines/macos-arm64-native-statements/SHA256SUMS']:+            self.assertTrue(live.is_input(path), path)
+ validation/test_release.py view
@@ -0,0 +1,91 @@+"""Release identity must preserve measurements, credentials and package boundaries."""+import hashlib+import importlib.util+from pathlib import Path+import tarfile+import tempfile+import unittest++import release_recorder as recorder+import run as frozen++ROOT = Path(__file__).resolve().parents[1]+++def tool(name):+    spec = importlib.util.spec_from_file_location(name, ROOT / 'tools' / (name + '.py'))+    module = importlib.util.module_from_spec(spec)+    spec.loader.exec_module(module)+    return module+++class RecorderIdentity(unittest.TestCase):+    def test_original_recorder_stays_exact(self):+        source = (ROOT / 'validation/run.py').read_bytes()+        self.assertEqual(recorder._FROZEN_SHA256, hashlib.sha256(source).hexdigest())+        original = source.decode().removesuffix(recorder._ENTRY_POINT)+        adapted = recorder._adapt_source(source)+        for old, new in recorder._SUBSTITUTIONS:+            adapted = adapted.replace(new, old)+        self.assertEqual(original, adapted)++    def test_workload_or_policy_edits_are_never_admitted(self):+        source = (ROOT / 'validation/run.py').read_bytes()+        for old, new in [(b'range(args.repetitions)', b'range(1)'),+                         (b'* 1.20', b'* 2.0'), (b'mysql-haskell.cabal', b'other.cabal')]:+            self.assertIn(old, source)+            with self.assertRaisesRegex(ValueError, 'frozen recorder changed'):+                recorder._adapt_source(source.replace(old, new))++    def test_frozen_measurement_functions_are_identical(self):+        # The functions keep their original code objects apart from module+        # metadata sites. A package rename cannot alter percentile calculations.+        self.assertEqual(frozen.percentile.__code__.co_code, recorder.percentile.__code__.co_code)+        self.assertEqual(frozen.compare.__code__.co_code, recorder.compare.__code__.co_code)++    def test_adapter_provenance_is_required_and_verified(self):+        proof = recorder.recorder_identity()+        files = {'validation/run.py': proof['frozen_source_sha256'],+                 'validation/release_recorder.py': proof['adapter_sha256']}+        recorder.verify_identity({'recorder_adapter': proof}, files)+        for build in [{}, {'recorder_adapter': {**proof, 'adapter_sha256': 'forged'}}]:+            with self.assertRaisesRegex(ValueError, 'metadata adapter'):+                recorder.verify_identity(build, files)+        with self.assertRaisesRegex(ValueError, 'metadata adapter'):+            recorder.verify_identity({'recorder_adapter': proof}, {**files, 'validation/run.py': 'changed'})+++class ReleaseSources(unittest.TestCase):+    def test_current_utf8_ca_names_and_checksum(self):+        updater = tool('update_ca_bundle')+        source = (ROOT / 'mozillaCAStore.pem').read_bytes()+        self.assertEqual(118, updater.validate(source, hashlib.sha256(source).hexdigest()))+        with self.assertRaisesRegex(ValueError, 'SHA-256'):+            updater.validate(source + b'changed', hashlib.sha256(source).hexdigest())++    def test_incomplete_or_empty_ca_bundle_is_rejected(self):+        updater = tool('update_ca_bundle')+        for source in [b'', b'-----BEGIN CERTIFICATE-----\ntruncated']:+            with self.assertRaisesRegex(ValueError, 'empty or incomplete'):+                updater.validate(source, hashlib.sha256(source).hexdigest())++    def test_private_traversal_and_special_archive_entries_are_rejected(self):+        checker = tool('check_release')+        for name, kind, reason in [+                ('package/../escape', tarfile.DIRTYPE, 'unexpected source archive path'),+                ('package/.local', tarfile.DIRTYPE, 'developer/private file leaked'),+                ('package/cabal.project', tarfile.REGTYPE, 'developer/private file leaked'),+                ('package/cabal.project.local', tarfile.REGTYPE, 'developer/private file leaked'),+                ('package/link', tarfile.SYMTYPE, 'unexpected special file')]:+            with self.subTest(name=name), tempfile.TemporaryDirectory() as folder:+                archive = Path(folder) / 'package.tar.gz'+                with tarfile.open(archive, 'w:gz') as output:+                    entry = tarfile.TarInfo(name)+                    entry.type = kind+                    output.addfile(entry)+                with self.assertRaisesRegex(ValueError, reason):+                    checker.unpack(archive, Path(folder) / 'unpacked', 'package')+++if __name__ == '__main__':+    unittest.main()
+ validation/test_runner.py view
@@ -0,0 +1,50 @@+#!/usr/bin/env python3+"""Regression-policy checks, independent of MariaDB and the driver."""+import copy+import unittest+import run+++class ComparisonTests(unittest.TestCase):+    def example(self):+        return {"correctness":[{"id":"one","status":"PASS"},{"id":"known","status":"FAIL"}],+                "build":{"workload_files":{"validation/run.py":"a"},"ghc":"9.10.3","cabal":"3.16.1"},+                "machine":{"machine":"arm64"},"server":{"identity":["MariaDB"]},+                "dependency_plan_sha256":"same", "benchmarks":{"read":{"status":"PASS","median_ns_per_unit":100}}}++    def test_pass_becoming_unsupported_is_a_regression(self):+        before=self.example(); after=copy.deepcopy(before)+        after["correctness"][0]["status"]="UNSUPPORTED"+        self.assertTrue(run.compare(after,before,False)["regressions"])++    def test_removing_test_is_a_regression(self):+        before=self.example(); after=copy.deepcopy(before); after["correctness"].pop(0)+        self.assertTrue(run.compare(after,before,False)["regressions"])++    def test_improvement_is_reported(self):+        before=self.example(); after=copy.deepcopy(before); after["correctness"][1]["status"]="PASS"+        self.assertEqual(run.compare(after,before,False)["improved_cases"],["known"])++    def test_slowdown_is_a_regression(self):+        before=self.example(); after=copy.deepcopy(before); after["benchmarks"]["read"]["median_ns_per_unit"]=121+        self.assertTrue(run.compare(after,before,True)["regressions"])++    def test_changed_environment_refuses_speed_comparison(self):+        before=self.example(); after=copy.deepcopy(before); after["machine"]={"machine":"x86_64"}+        self.assertTrue(run.compare(after,before,True)["regressions"])++    def test_known_failure_cannot_be_hidden_as_unavailable(self):+        before=self.example(); after=copy.deepcopy(before); after["correctness"][1]["status"]="ENVIRONMENT_UNAVAILABLE"+        self.assertTrue(run.compare(after,before,False)["regressions"])++    def test_percentile_uses_all_operation_samples(self):+        self.assertEqual(run.percentile([1,2,3,4,5],.5),3)+        self.assertAlmostEqual(run.percentile([1,2,3,4,5],.95),4.8)++    def test_new_failing_test_requires_attention(self):+        before=self.example(); after=copy.deepcopy(before)+        after["correctness"].append({"id":"new","status":"FAIL"})+        self.assertTrue(run.compare(after,before,False)["regressions"])+++if __name__ == "__main__": unittest.main()
+ validation/test_stage1_gates.py view
@@ -0,0 +1,180 @@+"""Keep every accepted improvement and reject incomparable measurements."""+import copy+import hashlib+import json+from pathlib import Path+import tempfile+import unittest+from stage1_gates import compare, reference++def data(status='PASS'):+    return {'correctness': [{'id': 'old.fixed', 'status': status}], 'benchmarks': {}, 'complete_suite': True, 'repetitions': 7, 'source_unchanged_during_run': True, 'machine': {}, 'server': {}, 'dependency_plan_sha256': 'same', 'connector_c_version': 'same', 'build': {'ghc': 'x', 'cabal': 'y', 'workload_files': {p: 'same' for p in ['Validation/Benchmark.hs', 'Validation/Common.hs', 'connector_c.c', 'run.py']}}}++class RatchetTests(unittest.TestCase):++    def test_previous_stage_pass_cannot_be_removed(self):+        published = data()+        published['correctness'].append({'id':'stage1.new-case','status':'PASS'})+        report = compare(data(), data(), data(), require_targets=False, previous_stage=published)+        self.assertFalse(report['correctness_accepted'])+        self.assertIn('previous_stage: removed stage1.new-case', report['correctness_failures'])++    def test_previous_stage_fixed_original_failure_cannot_regress(self):+        report = compare(data('FAIL'), data('FAIL'), data('FAIL'), require_targets=False,+                         previous_stage=data('PASS'))+        self.assertFalse(report['correctness_accepted'])++    def test_statement_targets_are_required_with_previous_stage(self):+        report = compare(data(), data(), data(), previous_stage=data())+        self.assertFalse(report['correctness_accepted'])+        self.assertTrue(any('required statement target' in item for item in report['correctness_failures']))++    def test_session_targets_are_required_with_previous_stage(self):+        report = compare(data(), data(), data(), previous_stage=data())+        self.assertFalse(report['correctness_accepted'])+        self.assertTrue(any('required session target' in item for item in report['correctness_failures']))++    def test_native_targets_are_required_with_previous_stage(self):+        report = compare(data(), data(), data(), previous_stage=data())+        self.assertFalse(report['correctness_accepted'])+        self.assertTrue(any('required native target' in item for item in report['correctness_failures']))++    def test_reintroduced_original_failure_is_caught(self):+        r = compare(data('FAIL'), data('PASS'), data('FAIL'), require_targets=False)+        self.assertFalse(r['correctness_accepted'])+        self.assertIn('accepted: lost PASS', r['correctness_failures'][0])++    def test_missing_case_is_caught(self):+        r = compare({'correctness': []}, data(), data(), require_targets=False)+        self.assertFalse(r['correctness_accepted'])++    def test_failure_cannot_become_inventory(self):+        self.assertFalse(compare(data('UNSUPPORTED'), data('FAIL'), data('FAIL'), require_targets=False)['correctness_accepted'])++    def test_new_inventory_cannot_hide_new_failure(self):+        c = data()+        c['correctness'].append({'id': 'stage1.new', 'status': 'UNSUPPORTED'})+        self.assertFalse(compare(c, data(), data(), require_targets=False)['correctness_accepted'])++    def test_known_deferred_failure_remains_visible(self):+        self.assertTrue(compare(data('FAIL'), data('FAIL'), data('FAIL'), require_targets=False)['correctness_accepted'])++    def test_inventory_case_cannot_introduce_an_unapproved_failure(self):+        for original_status in ['UNSUPPORTED', 'ENVIRONMENT_UNAVAILABLE']:+            for failure_status in ['FAIL', 'TIMEOUT']:+                with self.subTest(original=original_status, current=failure_status):+                    report = compare(data(failure_status), data(original_status),+                                     data(original_status), require_targets=False)+                    self.assertFalse(report['correctness_accepted'])++    def test_unknown_case_status_is_rejected(self):+        report = compare(data('NOT_RUN'), data('FAIL'), data('FAIL'), require_targets=False)+        self.assertFalse(report['correctness_accepted'])++    def test_new_failed_benchmark_is_not_accepted(self):+        current = data()+        current['benchmarks']['haskell/new'] = {'status': 'FAIL'}+        self.assertFalse(compare(current, data(), data(), require_targets=False)['correctness_accepted'])++    def test_previously_unsupported_benchmark_cannot_become_a_deferred_failure(self):+        original = data()+        original['benchmarks']['haskell/bulk'] = {'status': 'UNSUPPORTED'}+        current = copy.deepcopy(original)+        current['benchmarks']['haskell/bulk']['status'] = 'FAIL'+        self.assertFalse(compare(current, original, original, require_targets=False)['correctness_accepted'])++    def test_unsupported_benchmark_inventory_cannot_disappear(self):+        original = data()+        original['benchmarks']['haskell/bulk'] = {'status': 'UNSUPPORTED'}+        self.assertFalse(compare(data(), original, original, require_targets=False)['correctness_accepted'])++    def test_new_pass_is_accepted(self):+        self.assertTrue(compare(data('PASS'), data('FAIL'), data('FAIL'), require_targets=False)['correctness_accepted'])++    def test_timing_and_allocation_five_percent_trigger(self):+        old = data()+        old['benchmarks'] = {'haskell/x': {'status': 'PASS', 'median_ns_per_unit': 100, 'samples': [{'units': 1, 'allocated_bytes': 100}]}}+        new = copy.deepcopy(old)+        new['benchmarks']['haskell/x'].update(median_ns_per_unit=106, samples=[{'units': 1, 'allocated_bytes': 106}])+        r = compare(new, old, old, require_targets=False)+        self.assertTrue(r['performance_requires_confirmation'])+        self.assertEqual(len(r['allocation_investigations']), 2)++    def test_required_targets_cannot_be_omitted(self):+        self.assertFalse(compare(data(), data(), data())['correctness_accepted'])++    def test_profile_change_withholds_comparison(self):+        old = data()+        old['benchmarks'] = {'haskell/x': {'status': 'PASS', 'median_ns_per_unit': 100}}+        new = copy.deepcopy(old)+        new['machine'] = {'cpu': 'other'}+        new['benchmarks']['haskell/x']['median_ns_per_unit'] = 1+        report = compare(new, old, old, require_targets=False)+        self.assertFalse(report['evidence_accepted'])+        self.assertEqual([], report['timing_investigations'])++    def test_connector_version_change_withholds_comparison(self):+        old = data()+        old['benchmarks'] = {'connector-c/x': {'status': 'PASS', 'median_ns_per_unit': 100}}+        new = copy.deepcopy(old)+        new['connector_c_version'] = 'other'+        self.assertFalse(compare(new, old, old, require_targets=False)['evidence_accepted'])++    def test_partial_or_unstable_evidence_is_rejected(self):+        for key, value in [('complete_suite', False), ('repetitions', 1), ('source_unchanged_during_run', False)]:+            current = data()+            current[key] = value+            self.assertFalse(compare(current, data(), data(), require_targets=False)['evidence_accepted'])++    def test_workload_hash_change_is_not_a_speedup(self):+        old = data()+        old['benchmarks'] = {'haskell/x': {'status': 'PASS', 'median_ns_per_unit': 100}}+        new = copy.deepcopy(old)+        new['build']['workload_files']['run.py'] = 'changed'+        self.assertFalse(compare(new, old, old, require_targets=False)['evidence_accepted'])+class AcceptedReferenceTests(unittest.TestCase):+    def setUp(self):+        folder = tempfile.TemporaryDirectory()+        self.addCleanup(folder.cleanup)+        self.root = Path(folder.name) / "results"+        self.archive = self.root / "references/accepted.json"+        self.archive.parent.mkdir(parents=True)+        self.archive.write_text(json.dumps({"build": {"git_commit": "approved"}}))+        self.manifest = self.root / "accepted.json"++    def write_manifest(self, **changes):+        manifest = {"reference": "references/accepted.json", "tested_commit": "approved",+                    "sha256": hashlib.sha256(self.archive.read_bytes()).hexdigest()}+        manifest.update(changes)+        self.manifest.write_text(json.dumps(manifest))++    def test_approved_archive_is_loaded(self):+        self.write_manifest()+        accepted, manifest = reference(self.manifest)+        self.assertEqual("approved", accepted["build"]["git_commit"])+        self.assertEqual("approved", manifest["tested_commit"])++    def test_modified_reference_is_rejected(self):+        self.write_manifest(sha256="0" * 64)+        with self.assertRaisesRegex(ValueError, "hash mismatch"):+            reference(self.manifest)++    def test_manifest_commit_must_match_archive(self):+        self.write_manifest(tested_commit="different")+        with self.assertRaisesRegex(ValueError, "tested commit"):+            reference(self.manifest)++    def test_live_candidate_cannot_be_the_reference(self):+        (self.root / "current.json").write_bytes(self.archive.read_bytes())+        self.write_manifest(reference="current.json")+        with self.assertRaisesRegex(ValueError, "own accepted reference"):+            reference(self.manifest)++    def test_reference_cannot_escape_results_directory(self):+        self.write_manifest(reference="../outside.json")+        with self.assertRaisesRegex(ValueError, "escapes"):+            reference(self.manifest)+++if __name__ == '__main__':+    unittest.main()
+ validation/test_stage3_benchmarks.py view
@@ -0,0 +1,231 @@+"""Guard native-feature evidence units and identity, independently of Haskell."""+import unittest+import copy+import statistics+import json+import hashlib+from pathlib import Path+import tempfile+from unittest.mock import patch+import stage3_benchmarks as native+from stage3_benchmarks import sample, verify_evidence, PROFILES, PAIRS+++class NativeSampleTests(unittest.TestCase):+    def record(self, ident='bulk.native', units=5000, checksum=3147500):+        return f'STAGE3\t{ident}\t{units}\t100\t200\t100\t10\t{checksum}\n'++    def test_valid_bulk_record(self):+        self.assertEqual(sample(self.record(), 'bulk.native')['units'], 5000)++    def test_missing_record(self):+        with self.assertRaises(ValueError):+            sample('', 'bulk.native')++    def test_duplicate_record(self):+        with self.assertRaises(ValueError):+            sample(self.record() * 2, 'bulk.native')++    def test_identity_mismatch(self):+        with self.assertRaises(ValueError):+            sample(self.record(), 'bulk.units')++    def test_checksum_mismatch(self):+        with self.assertRaises(ValueError):+            sample(self.record(checksum=1), 'bulk.native')++    def test_unit_mismatch(self):+        with self.assertRaises(ValueError):+            sample(self.record(units=1000), 'bulk.native')++    def test_zero_work(self):+        with self.assertRaises(ValueError):+            sample(self.record(units=0), 'bulk.native')++    def test_negative_counter(self):+        with self.assertRaises(ValueError):+            sample(self.record().replace('\t200\t', '\t-200\t'), 'bulk.native')++    def test_zero_allocation_cannot_be_a_comparison_denominator(self):+        with self.assertRaises(ValueError):+            sample(self.record().replace('\t200\t', '\t0\t'), 'bulk.native')++    def test_wide_query_checksum_and_units(self):+        result = sample(self.record('prepared.wide.cached', 500, 1923500), 'prepared.wide.cached')+        self.assertEqual(result['checksum'], 1923500)+++class NativeEvidenceTests(unittest.TestCase):+    def setUp(self):+        self.inputs = {'sha256': 'current', 'files': {name: 'digest' for name in ['validation/hs/Validation/Stage3Bench.hs', 'validation/hs/Validation/Common.hs', 'validation/stage3_benchmarks.py']}}+        self.data = {'complete': True, 'repetitions': 7, 'inputs': self.inputs,+                     'profiles': {}, 'comparisons': {}}+        self.data.update(server={}, machine={}, dependencies=[], haskell_benchmark_rts=['-N4', '-T'], build={'ghc': 'compiler', 'cabal': 'tool'})+        for ident, iterations in PROFILES.items():+            units = iterations * (1000 if ident.startswith('bulk.') else 1)+            checksum = iterations * (629500 if ident.startswith('bulk.') else 3847 if '.wide.' in ident else 7)+            entry = {'units': units, 'elapsed_ns': 100, 'allocated_bytes': 200,+                     'cpu_ns': 80, 'gc_cpu_ns': 10, 'checksum': checksum}+            profile = {'samples': [copy.deepcopy(entry) for _ in range(7)]}+            for key in ('elapsed_ns', 'allocated_bytes', 'cpu_ns', 'gc_cpu_ns'):+                profile['median_' + key + '_per_unit'] = statistics.median([entry[key] / units] * 7)+            self.data['profiles'][ident] = profile+        for name, (candidate, reference) in PAIRS.items():+            self.data['comparisons'][name] = {'current': candidate, 'baseline': reference,+                'change_percent': {key: 100 * (self.data['profiles'][candidate]['median_' + key + '_per_unit'] /+                    self.data['profiles'][reference]['median_' + key + '_per_unit'] - 1)+                    for key in ('elapsed_ns', 'allocated_bytes')}}+        folder = tempfile.TemporaryDirectory()+        self.addCleanup(folder.cleanup)+        self.directory = Path(folder.name) / native.BASELINE_DIRECTORY+        self.directory.mkdir(parents=True)+        self.write_baseline()+        location = patch.object(native.run, 'HERE', Path(folder.name))+        location.start()+        self.addCleanup(location.stop)+        native.compare_native_baseline(self.data)++    def write_baseline(self):+        artifacts = {'results.json': (json.dumps(self.data) + '\n').encode(),+                     'SUMMARY.md': b'Immutable native baseline fixture\n'}+        for name, raw in artifacts.items():+            (self.directory / name).write_bytes(raw)+        (self.directory / 'SHA256SUMS').write_text(''.join(+            hashlib.sha256(raw).hexdigest() + '  ' + name + '\n'+            for name, raw in artifacts.items()))++    def test_complete_evidence(self):+        verify_evidence(self.data, self.inputs)++    def test_stale_evidence(self):+        with self.assertRaisesRegex(ValueError, 'stale'):+            verify_evidence(self.data, {'sha256': 'new'})++    def test_partial_evidence(self):+        self.data['complete'] = False+        with self.assertRaises(ValueError):+            verify_evidence(self.data, self.inputs)++    def test_missing_profile(self):+        del self.data['profiles']['bulk.units']+        with self.assertRaises(ValueError):+            verify_evidence(self.data, self.inputs)++    def test_missing_repetition(self):+        self.data['profiles']['bulk.native']['samples'].pop()+        with self.assertRaises(ValueError):+            verify_evidence(self.data, self.inputs)++    def test_changed_median(self):+        self.data['profiles']['bulk.native']['median_elapsed_ns_per_unit'] = 1+        with self.assertRaisesRegex(ValueError, 'summary'):+            verify_evidence(self.data, self.inputs)++    def test_changed_checksum(self):+        self.data['profiles']['bulk.native']['samples'][0]['checksum'] = 1+        with self.assertRaisesRegex(ValueError, 'checksum'):+            verify_evidence(self.data, self.inputs)++    def test_changed_comparison_reference(self):+        self.data['comparisons']['native_vs_sequential']['baseline'] = 'bulk.units'+        with self.assertRaisesRegex(ValueError, 'identity'):+            verify_evidence(self.data, self.inputs)++    def test_changed_comparison_percentage(self):+        self.data['comparisons']['native_vs_sequential']['change_percent']['elapsed_ns'] = 99+        with self.assertRaisesRegex(ValueError, 'comparison'):+            verify_evidence(self.data, self.inputs)++    def test_missing_baseline_comparison(self):+        del self.data['baseline_comparison']+        with self.assertRaisesRegex(ValueError, 'baseline comparison'):+            verify_evidence(self.data, self.inputs)++    def test_publication_rejects_deleted_baseline(self):+        (self.directory / 'results.json').unlink()+        with self.assertRaisesRegex(ValueError, 'required native baseline'):+            verify_evidence(self.data, self.inputs)++    def test_publication_rejects_changed_baseline(self):+        (self.directory / 'results.json').write_text('{}')+        with self.assertRaisesRegex(ValueError, 'checksum'):+            verify_evidence(self.data, self.inputs)++    def test_publication_recalculates_baseline_deltas(self):+        self.data['baseline_comparison']['profiles']['bulk.native']['elapsed_ns'] = 99+        with self.assertRaisesRegex(ValueError, 'baseline comparison'):+            verify_evidence(self.data, self.inputs)++    def test_publication_rejects_changed_baseline_alarms(self):+        self.data['baseline_comparison']['timing_investigations'] = [{'id': 'bulk.native', 'percent': 25}]+        with self.assertRaisesRegex(ValueError, 'baseline comparison'):+            verify_evidence(self.data, self.inputs)++    def test_publication_rejects_false_profile_mismatch(self):+        self.data['baseline_comparison']['comparable'] = False+        with self.assertRaisesRegex(ValueError, 'baseline comparison'):+            verify_evidence(self.data, self.inputs)+++class NativeBaselineTests(unittest.TestCase):+    setUp = NativeEvidenceTests.setUp+    write_baseline = NativeEvidenceTests.write_baseline+    def compare(self, mutation=None, bad_hash=False):+        if mutation:+            mutation()+        if bad_hash:+            (self.directory / 'results.json').write_text('{}')+        native.compare_native_baseline(self.data)+        return self.data['baseline_comparison']++    def test_same_profile_uses_immutable_capture(self):+        comparison = self.compare()+        self.assertTrue(comparison['comparable'])+        self.assertEqual([], comparison['timing_investigations'])++    def test_changed_server_withholds_deltas(self):+        comparison = self.compare(lambda: self.data.update(server={'changed': True}))+        self.assertFalse(comparison['comparable'])+        self.assertEqual({}, comparison['profiles'])++    def test_timing_alarm_is_retained(self):+        def slow():+            self.data['profiles']['bulk.native']['median_elapsed_ns_per_unit'] *= 1.25+        comparison = self.compare(slow)+        self.assertAlmostEqual(25, comparison['timing_investigations'][0]['percent'])++    def test_baseline_tampering_is_rejected(self):+        with self.assertRaisesRegex(ValueError, 'checksum'):+            self.compare(bad_hash=True)++    def test_missing_baseline_is_rejected(self):+        (self.directory / 'results.json').unlink()+        with self.assertRaisesRegex(ValueError, 'required native baseline'):+            self.compare()++    def test_missing_checksum_manifest_is_rejected(self):+        (self.directory / 'SHA256SUMS').unlink()+        with self.assertRaisesRegex(ValueError, 'required native baseline'):+            self.compare()++    def test_changed_summary_is_rejected(self):+        (self.directory / 'SUMMARY.md').write_text('changed')+        with self.assertRaisesRegex(ValueError, 'checksum'):+            self.compare()++    def test_dependency_tuples_match_their_json_representation(self):+        self.data['dependencies'] = [('example', '1.0')]+        self.write_baseline()+        self.assertTrue(self.compare()['comparable'])++    def test_policy_fix_preserves_identical_haskell_workload_comparison(self):+        self.inputs['files']['validation/stage3_benchmarks.py'] = 'policy fix'+        self.assertTrue(self.compare()['comparable'])++    def test_changed_workload_withholds_deltas(self):+        self.inputs['files'][native.WORKLOAD_PATHS[0]] = 'changed workload'+        self.assertFalse(self.compare()['comparable'])+++if __name__ == '__main__':+    unittest.main()
+ validation/test_stage4_benchmarks.py view
@@ -0,0 +1,159 @@+"""Independently verify reuse work, immutable baselines and publication evidence."""+import copy+import hashlib+import json+from pathlib import Path+import tempfile+import unittest+from unittest.mock import patch++import feature_benchmarks as features+import stage4_benchmarks as reuse+++class ReuseEvidenceTests(unittest.TestCase):+    def setUp(self):+        self.spec = reuse.specification()+        self.inputs = {'sha256': 'current', 'commit': 'code',+                       'files': {name: 'digest' for name in reuse.WORKLOAD_PATHS}}+        self.data = {'complete': True, 'repetitions': 7, 'inputs': copy.deepcopy(self.inputs),+                     'server': {}, 'machine': {}, 'dependencies': [],+                     'haskell_benchmark_rts': ['-N4', '-T'],+                     'build': {'ghc': 'compiler', 'cabal': 'tool', 'git_commit': 'code'},+                     'profiles': {}, 'comparisons': {}}+        for ident, count in reuse.PROFILES.items():+            sample = {'units': count, 'elapsed_ns': count * 100, 'allocated_bytes': count * 200,+                      'cpu_ns': count * 80, 'gc_cpu_ns': count * 10, 'checksum': count}+            profile = {'samples': [copy.deepcopy(sample) for _ in range(7)]}+            for key in ('elapsed_ns', 'allocated_bytes', 'cpu_ns', 'gc_cpu_ns'):+                profile['median_' + key + '_per_unit'] = sample[key] / count+            self.data['profiles'][ident] = profile+        for name, (current, baseline) in reuse.PAIRS.items():+            self.data['comparisons'][name] = {'current': current, 'baseline': baseline,+                'change_percent': {'elapsed_ns': 0.0, 'allocated_bytes': 0.0}}+        folder = tempfile.TemporaryDirectory()+        self.addCleanup(folder.cleanup)+        root = Path(folder.name)+        self.directory = root / reuse.BASELINE_DIRECTORY+        self.directory.mkdir(parents=True)+        artifacts = {'results.json': json.dumps(self.data).encode(), 'SUMMARY.md': b'first reuse capture\n'}+        for name, raw in artifacts.items():+            (self.directory / name).write_bytes(raw)+        (self.directory / 'SHA256SUMS').write_text(''.join(+            hashlib.sha256(raw).hexdigest() + '  ' + name + '\n' for name, raw in artifacts.items()))+        location = patch.object(features.run, 'HERE', root)+        location.start()+        self.addCleanup(location.stop)+        features.attach_baseline_comparison(self.spec, self.data)++    def verify(self):+        reuse.verify_evidence(self.data, self.inputs)++    def test_complete_data(self):+        self.verify()++    def test_wrong_units_for_same_checksum(self):+        self.data['profiles']['reuse.tcp.reset']['samples'][0]['units'] = 1+        with self.assertRaisesRegex(ValueError, 'unit count'):+            self.verify()++    def test_wrong_checksum(self):+        self.data['profiles']['reuse.tls.reset']['samples'][0]['checksum'] = 0+        with self.assertRaisesRegex(ValueError, 'checksum'):+            self.verify()++    def test_snapshot_has_its_own_unit_count(self):+        self.data['profiles']['session.snapshot']['samples'][0]['units'] = 30+        with self.assertRaisesRegex(ValueError, 'unit count'):+            self.verify()++    def test_missing_transport(self):+        del self.data['profiles']['reuse.socket.reconnect']+        with self.assertRaisesRegex(ValueError, 'profiles'):+            self.verify()++    def test_missing_repetition(self):+        self.data['profiles']['reuse.tcp.reset']['samples'].pop()+        with self.assertRaisesRegex(ValueError, 'repetitions'):+            self.verify()++    def test_edited_median(self):+        self.data['profiles']['reuse.tcp.reset']['median_elapsed_ns_per_unit'] = 1+        with self.assertRaisesRegex(ValueError, 'summary'):+            self.verify()++    def test_missing_comparison(self):+        del self.data['comparisons']['tls']+        with self.assertRaisesRegex(ValueError, 'comparison profiles'):+            self.verify()++    def test_reversed_comparison(self):+        self.data['comparisons']['tcp']['baseline'] = 'reuse.tcp.reset'+        with self.assertRaisesRegex(ValueError, 'identity'):+            self.verify()++    def test_edited_change(self):+        self.data['comparisons']['tcp']['change_percent']['elapsed_ns'] = -50+        with self.assertRaisesRegex(ValueError, 'comparison'):+            self.verify()++    def test_old_input_fingerprint(self):+        self.data['inputs']['sha256'] = 'old'+        with self.assertRaisesRegex(ValueError, 'stale'):+            self.verify()++    def test_same_digest_cannot_hide_different_files(self):+        self.data['inputs']['files'][reuse.WORKLOAD_PATHS[0]] = 'fake mismatch'+        with self.assertRaisesRegex(ValueError, 'input file'):+            self.verify()++    def test_wrong_tested_commit(self):+        self.data['build']['git_commit'] = 'old'+        with self.assertRaisesRegex(ValueError, 'tested commit'):+            self.verify()++    def test_missing_baseline_is_rejected(self):+        (self.directory / 'results.json').unlink()+        with self.assertRaisesRegex(ValueError, 'required session reuse baseline'):+            self.verify()++    def test_changed_baseline_is_rejected(self):+        (self.directory / 'results.json').write_text('{}')+        with self.assertRaisesRegex(ValueError, 'checksum'):+            self.verify()++    def test_changed_summary_is_rejected(self):+        (self.directory / 'SUMMARY.md').write_text('different')+        with self.assertRaisesRegex(ValueError, 'checksum'):+            self.verify()++    def test_omitted_baseline_comparison(self):+        del self.data['baseline_comparison']+        with self.assertRaisesRegex(ValueError, 'baseline comparison'):+            self.verify()++    def test_cannot_hide_comparability(self):+        self.data['baseline_comparison']['comparable'] = False+        with self.assertRaisesRegex(ValueError, 'baseline comparison'):+            self.verify()++    def test_cannot_hide_timing_alarm(self):+        self.data['baseline_comparison']['timing_investigations'] = [{'id': 'x', 'percent': 99}]+        with self.assertRaisesRegex(ValueError, 'baseline comparison'):+            self.verify()++    def test_environment_change_withholds_deltas(self):+        self.data['server'] = {'different': True}+        comparison = features.baseline_comparison(self.spec, self.data)+        self.assertFalse(comparison['comparable'])+        self.assertEqual({}, comparison['profiles'])++    def test_duplicate_and_wrong_markers(self):+        line = 'STAGE4\treuse.tcp.reset\t30\t100\t200\t80\t10\t30\n'+        for content in (line * 2, line.replace('STAGE4', 'STAGE3')):+            with self.assertRaises(ValueError):+                features.sample(self.spec, content, 'reuse.tcp.reset')+++if __name__ == '__main__':+    unittest.main()
+ validation/test_stage5.py view
@@ -0,0 +1,137 @@+"""CDC measurements require complete work, bounded heap and all new targets."""+import copy+import unittest++import feature_benchmarks as features+import publish_results as live+import stage5_benchmarks as cdc+import stage1_gates as gates+++class CDCEvidenceTests(unittest.TestCase):+    def setUp(self):+        self.spec = cdc.specification()+        self.heap = [[n,100000] for n in (1000,10000,100000,1000000)]*3++    def test_heap_complete(self):+        features.verify_heap(self.spec, 'cdc.retention', self.heap)++    def test_heap_missing_warmup(self):+        with self.assertRaisesRegex(ValueError, 'incomplete'):+            features.verify_heap(self.spec, 'cdc.retention', self.heap[4:])++    def test_heap_wrong_count(self):+        self.heap = copy.deepcopy(self.heap)+        self.heap[-1][0] = 999999+        with self.assertRaisesRegex(ValueError, 'incomplete'):+            features.verify_heap(self.spec, 'cdc.retention', self.heap)++    def test_heap_growth(self):+        self.heap = copy.deepcopy(self.heap)+        self.heap[-1][1] += 3*1024*1024+        with self.assertRaisesRegex(ValueError, 'grows'):+            features.verify_heap(self.spec, 'cdc.retention', self.heap)++    def test_crc_workload_uses_event_checksum(self):+        line='STAGE5\tcdc.framing.crc32\t20000\t100000\t200000\t100000\t1000\t20000'+        features.sample(self.spec, line, 'cdc.framing.crc32')+        with self.assertRaisesRegex(ValueError, 'checksum'):+            features.sample(self.spec, line[:-5]+'10000', 'cdc.framing.crc32')++    def test_cdc_baselines_are_inputs(self):+        for name in ('results.json','SUMMARY.md','SHA256SUMS'):+            self.assertTrue(live.is_input('validation/baselines/macos-arm64-cdc/'+name))++    def test_required_cdc_targets(self):+        self.assertEqual(18, len(gates.CDC_TARGETS))+        self.assertIn('cdc.gtid-resume', gates.CDC_TARGETS)+        self.assertIn('cdc.durable-ack-control', gates.CDC_TARGETS)+++class CDCPublicationTests(unittest.TestCase):+    def fixture(self):+        inputs={'sha256':'source','files':{},'commit':'code','dirty_inputs':[]}+        attempt={'state':'COMPLETED','inputs':inputs}+        data={'complete_suite':True,'repetitions':7,'source_unchanged_during_run':True,+              'correctness':[{'id':'stage5.fixture','status':'PASS'}], 'build':{'git_commit':'code'}}+        return {'latest_attempt':attempt,'latest_completed_run':{'attempt':attempt,'results':data}},inputs++    def test_missing_cdc_evidence_blocks_publication(self):+        document,inputs=self.fixture()+        with self.assertRaisesRegex(ValueError, 'CDC'):+            live.verify(document,inputs)++    def test_cdc_evidence_is_reverified(self):+        from unittest.mock import patch+        document,inputs=self.fixture()+        document['latest_attempt']['cdc']={'captured':'evidence'}+        with patch('stage5_benchmarks.verify_evidence') as verify:+            live.verify(document,inputs)+        verify.assert_called_once_with({'captured':'evidence'},inputs)++    def test_bad_heap_shape_is_rejected(self):+        with self.assertRaisesRegex(ValueError, 'incomplete'):+            features.verify_heap(cdc.specification(),'cdc.retention',[[1000,'unknown']]*12)+++class CDCWrapperTests(unittest.TestCase):+    def check_capture(self, failure, expected_status, timing=()):+        import json+        from pathlib import Path+        import tempfile+        from unittest.mock import patch+        import check+        with tempfile.TemporaryDirectory() as folder:+            root=Path(folder)+            (root/'results').mkdir()+            output=root/'run'+            config=root/'config.json'+            config.write_text('{}')+            inputs={'sha256':'candidate','files':{},'commit':'parent'}+            candidate={'complete_suite':True,'correctness':[{'id':'stage5.fixture','status':'PASS'}]}+            gate={'correctness_accepted':True,'evidence_accepted':True}+            measurements={'baseline_comparison':{'timing_investigations':list(timing),'allocation_investigations':[]}}+            def recorder(*args,**kwargs):+                output.mkdir()+                (output/'results.json').write_text(json.dumps(candidate))+                return 0+            with (patch('sys.argv',['check.py','--working-tree','--config',str(config),'--output',str(output)]),+                  patch.object(check.live,'input_snapshot',return_value=inputs),+                  patch.object(check.run,'HERE',root),+                  patch.object(check.run,'sql_client',return_value='12.3.3-MariaDB'),+                  patch.object(check.subprocess,'run') as build,+                  patch.object(check.subprocess,'call',side_effect=recorder),+                  patch.object(check,'install_runtime_data',return_value=0),+                  patch.object(check.stage1_gates,'reference',return_value=({},{})),+                  patch.object(check.stage1_gates,'compare',return_value=gate),+                  patch.object(check,'native_benchmarks',return_value={}),+                  patch('stage3_benchmarks.verify_evidence'),+                  patch.object(check,'cdc_benchmarks',return_value=measurements,side_effect=failure) as capture,+                  patch('stage5_benchmarks.verify_evidence'),+                  patch.object(check.live,'publish') as publish):+                build.return_value.returncode=0+                self.assertEqual(expected_status,check.main())+            capture.assert_called_once_with(output.resolve(),7,config)+            attempt,data=publish.call_args.args+            if failure is None:+                self.assertEqual('COMPLETED',attempt['state'])+                self.assertEqual(measurements,attempt['cdc'])+                self.assertEqual(candidate,data)+                if timing:+                    self.assertTrue(gate['performance_requires_confirmation'])+            else:+                self.assertEqual('FAILED',attempt['state'])+                self.assertEqual('CDC benchmarks',attempt['stage'])+                self.assertIsNone(data)++    def test_recorder_failure_is_visible(self):+        self.check_capture(RuntimeError('incomplete CDC capture'),1)++    def test_interruption_is_visible(self):+        self.check_capture(KeyboardInterrupt(),130)++    def test_small_timing_signal_stays_open(self):+        self.check_capture(None,0,[{'id':'cdc.live.rows','percent':6}])++    def test_large_timing_signal_fails_wrapper(self):+        self.check_capture(None,1,[{'id':'cdc.live.rows','percent':21}])
+ validation/test_stage6.py view
@@ -0,0 +1,165 @@+"""Optional-wire evidence cannot disappear from a complete Stage 6 report."""+import unittest+from unittest.mock import patch++import feature_benchmarks as features+import publish_results as live+import stage6_benchmarks as wire+++class WireEvidenceTests(unittest.TestCase):+    def test_expected_work_and_checksum(self):+        spec = wire.specification()+        self.assertEqual(13, len(spec.profiles))+        self.assertEqual(7, len(spec.pairs))+        for name in spec.profiles:+            line = 'STAGE6\t' + name + '\t' + '\t'.join(map(str,+                (spec.units[name], 1000, 2000, 100, 0, spec.checksums[name])))+            features.sample(spec, line, name)+            with self.assertRaisesRegex(ValueError, 'checksum'):+                features.sample(spec, line.rsplit('\t', 1)[0]+'\t0', name)++    def test_baseline_artifacts_are_inputs(self):+        for name in ('results.json', 'SUMMARY.md', 'SHA256SUMS'):+            self.assertTrue(live.is_input('validation/baselines/macos-arm64-wire-features/'+name))++    def test_retention_requires_all_upload_checkpoints(self):+        spec = wire.specification()+        checkpoints = (65536, 1048576, 16777216, 67108864)+        self.assertEqual(checkpoints, spec.heap_profiles['upload.retention'])+        entries = [[n, 100000] for n in checkpoints] * 3+        features.verify_heap(spec, 'upload.retention', entries)+        with self.assertRaisesRegex(ValueError, 'incomplete'):+            features.verify_heap(spec, 'upload.retention', entries[4:])+        entries[-1][1] += 3 * 1024 * 1024+        with self.assertRaisesRegex(ValueError, 'grows'):+            features.verify_heap(spec, 'upload.retention', entries)++    def test_retention_rejects_growth_released_before_final_checkpoint(self):+        spec = wire.specification()+        entries = [[n, 100000] for n in spec.heap_profiles['upload.retention']] * 3+        # Fresh lists prevent changing another pass's shared test fixture.+        entries = [entry[:] for entry in entries]+        entries[2][1] += 3 * 1024 * 1024+        with self.assertRaisesRegex(ValueError, 'grows'):+            features.verify_heap(spec, 'upload.retention', entries)++    def test_buffering_control_must_fail_for_growth(self):+        import subprocess+        checkpoints = wire.specification().heap_profiles['upload.retention']+        small = ''.join(f'STAGE6_HEAP\t{n}\t100000\n' for n in checkpoints) * 3+        with patch.object(wire.run, 'command', return_value=subprocess.CompletedProcess([], 0, stdout=small)):+            with self.assertRaisesRegex(ValueError, 'escaped'):+                wire.verify_buffering_control('fixture')+        grown = ''.join(f'STAGE6_HEAP\t{n}\t{100000+n}\n' for n in checkpoints) * 3+        with patch.object(wire.run, 'command', return_value=subprocess.CompletedProcess([], 0, stdout=grown)), patch('builtins.print'):+            wire.verify_buffering_control('fixture')++    def fixture(self):+        inputs = {'sha256':'source', 'files':{}, 'commit':'code', 'dirty_inputs':[]}+        attempt = {'state':'COMPLETED', 'inputs':inputs}+        data = {'complete_suite':True, 'repetitions':7, 'source_unchanged_during_run':True,+                'correctness':[{'id':'stage6.fixture', 'status':'PASS'}], 'build':{'git_commit':'code'}}+        return {'latest_attempt':attempt, 'latest_completed_run':{'attempt':attempt, 'results':data}}, inputs++    def test_missing_wire_evidence_blocks_publication(self):+        document, inputs = self.fixture()+        with self.assertRaisesRegex(ValueError, 'Wire features'):+            live.verify(document, inputs)++    def test_wire_evidence_is_reverified(self):+        document, inputs = self.fixture()+        document['latest_attempt']['wire_features'] = {'captured':'evidence'}+        with patch('stage6_benchmarks.verify_evidence') as verify:+            live.verify(document, inputs)+        verify.assert_called_once_with({'captured':'evidence'}, inputs)+++class WireWrapperTests(unittest.TestCase):+    def test_changed_build_inputs_are_rejected_before_recording(self):+        import check+        import tempfile+        from pathlib import Path+        with tempfile.TemporaryDirectory() as folder:+            config = Path(folder)/'config.json'+            config.write_text('{}')+            before = {'files':{'source.hs':'old'}, 'commit':'parent', 'branch':'main'}+            after = dict(before, files={'source.hs':'new'})+            with (patch('sys.argv', ['check.py','--working-tree','--config',str(config),'--output',str(Path(folder)/'run')]),+                  patch.object(check.live,'input_snapshot',side_effect=[before,after]),+                  patch.object(check.run,'sql_client',return_value='12.3.3-MariaDB'),+                  patch.object(check.subprocess,'run') as build,+                  patch.object(check.subprocess,'call') as recorder,+                  patch.object(check.live,'publish') as publish):+                build.return_value.returncode = 0+                self.assertEqual(1,check.main())+            recorder.assert_not_called()+            attempt,data = publish.call_args.args+            self.assertEqual('FAILED',attempt['state'])+            self.assertIn('changed during build',attempt['reason'])+            self.assertIsNone(data)++    def check_capture(self, failure, expected_status, timing=()):+        import json+        from pathlib import Path+        import tempfile+        from unittest.mock import patch+        import check+        with tempfile.TemporaryDirectory() as folder:+            root=Path(folder)+            (root/'results').mkdir()+            output=root/'run'+            config=root/'config.json'+            config.write_text('{}')+            inputs={'sha256':'candidate','files':{},'commit':'parent'}+            candidate={'complete_suite':True,'correctness':[{'id':'stage6.fixture','status':'PASS'}]}+            gate={'correctness_accepted':True,'evidence_accepted':True}+            measurements={'baseline_comparison':{'timing_investigations':list(timing),'allocation_investigations':[]}}+            def recorder(*args,**kwargs):+                output.mkdir()+                (output/'results.json').write_text(json.dumps(candidate))+                return 0+            with (patch('sys.argv',['check.py','--working-tree','--config',str(config),'--output',str(output)]),+                  patch.object(check.live,'input_snapshot',return_value=inputs),+                  patch.object(check.run,'HERE',root),+                  patch.object(check.run,'sql_client',return_value='12.3.3-MariaDB'),+                  patch.object(check.subprocess,'run') as build,+                  patch.object(check.subprocess,'call',side_effect=recorder),+                  patch.object(check,'install_runtime_data',return_value=0),+                  patch.object(check.stage1_gates,'reference',return_value=({},{})),+                  patch.object(check.stage1_gates,'compare',return_value=gate),+                  patch.object(check,'native_benchmarks',return_value={}),+                  patch('stage3_benchmarks.verify_evidence'),+                  patch.object(check,'wire_benchmarks',return_value=measurements,side_effect=failure) as capture,+                  patch('stage6_benchmarks.verify_evidence'),+                  patch.object(check.live,'publish') as publish):+                build.return_value.returncode=0+                self.assertEqual(expected_status,check.main())+            capture.assert_called_once_with(output.resolve(),7,config)+            attempt,data=publish.call_args.args+            if failure is None:+                self.assertEqual('COMPLETED',attempt['state'])+                self.assertEqual(measurements,attempt['wire_features'])+                self.assertEqual(candidate,data)+                if timing:+                    self.assertTrue(gate['performance_requires_confirmation'])+            else:+                self.assertEqual('FAILED',attempt['state'])+                self.assertEqual('wire feature benchmarks',attempt['stage'])+                self.assertIsNone(data)++    def test_recorder_failure_is_visible(self):+        self.check_capture(RuntimeError('incomplete wire capture'),1)++    def test_interruption_is_visible(self):+        self.check_capture(KeyboardInterrupt(),130)++    def test_small_timing_signal_stays_open(self):+        self.check_capture(None,0,[{'id':'transport.wide.zlib','percent':6}])++    def test_large_timing_signal_fails_wrapper(self):+        self.check_capture(None,1,[{'id':'transport.wide.zlib','percent':21}])+++if __name__ == '__main__':+    unittest.main()
+ validation/test_stage7.py view
@@ -0,0 +1,243 @@+"""Preserve the approved contract correction and real external-auth evidence."""+import copy+from contextlib import contextmanager+import json+from pathlib import Path+import re+import subprocess+import sys+import tempfile+import unittest+from unittest.mock import patch++import check+import feature_benchmarks as features+import publish_results as live+import stage1_gates+import stage7_benchmarks as auth+import auth_fixture+++class ExternalAuthenticationPolicyTests(unittest.TestCase):+    def test_partial_setup_failures_never_publish_full_status(self):+        variants = [['--only=protocol.length-0'], ['--only', 'stage7.live.'],+                    ['--repetitions', '1'], ['--repetitions=6'],+                    ['--no-upstream'], ['--no-benchmarks']]+        for arguments in variants:+            for error in [RuntimeError('fixture failed'), KeyboardInterrupt()]:+                with self.subTest(arguments=arguments, error=type(error).__name__), \+                     patch('sys.argv', ['check.py', *arguments]), \+                     patch.object(check, '_main_with_authentication', side_effect=error), \+                     patch.object(check.live, 'publish') as publish:+                    self.assertEqual(130 if isinstance(error, KeyboardInterrupt) else 1,+                                     check.main_with_authentication())+                    publish.assert_not_called()++    def test_full_fixture_interruption_is_published(self):+        with patch('sys.argv', ['check.py', '--working-tree']), \+             patch.object(check, '_main_with_authentication', side_effect=KeyboardInterrupt()), \+             patch.object(check.live, 'input_snapshot', return_value={'files': {}}), \+             patch.object(check.live, 'publish') as publish:+            self.assertEqual(130, check.main_with_authentication())+        attempt = publish.call_args.args[0]+        self.assertEqual('FAILED', attempt['state'])+        self.assertEqual(130, attempt['exit_code'])++    def test_non_live_filters_do_not_provision_services(self):+        for selected in ['protocol.length-0', 'stage7.dialog.', 'stage7.wire.',+                         'stage7.clear-password.', 'stage7.gssapi.wrong-principal']:+            with self.subTest(selected=selected), \+                 patch('sys.argv', ['check.py', '--only='+selected]), \+                 patch.object(check, 'main', return_value=0), \+                 patch.object(check.run, 'sql_client') as preflight, \+                 patch.object(auth_fixture, 'environment') as fixture:+                self.assertEqual(0, check.main_with_authentication())+                fixture.assert_not_called()+                preflight.assert_not_called()++    def test_live_and_complete_selections_require_fixtures(self):+        for arguments in [[], ['--no-benchmarks'], ['--only', 'stage7'],+                          ['--only', 'gssapi'], ['--only', 'auth.pam-environment'],+                          ['--only', 'stage7.live.pam-wrong-password']]:+            with self.subTest(arguments=arguments):+                self.assertTrue(check.needs_authentication_fixture(check.parse_arguments(arguments)))+        source = (live.ROOT/'validation/hs/Validation/Stage7.hs').read_text()+        required = {'stage7.'+name for name in re.findall(r'case7\s+"(live\.[^"]+)"', source)}+        self.assertTrue(required)+        self.assertTrue(required.issubset(check.AUTHENTICATION_FIXTURE_CASES))++    def test_private_fixture_config_preserves_argv_and_caller_settings(self):+        with tempfile.TemporaryDirectory() as folder:+            root = Path(folder)+            (root/'.local').mkdir()+            config = root/'config.json'+            config.write_text(json.dumps({'MB_USER': 'caller', 'MB_PASSWORD': 'private'}))+            arguments = ['check.py', '--config', str(config), '--only=stage7.live.', '--unknown-option']+            @contextmanager+            def fixture():+                yield {'MB_AUTH_HOST': 'fixture', 'KRB5CCNAME': 'private-cache'}+            def run(args):+                settings = json.loads(args.config.read_text())+                self.assertEqual('caller', settings['MB_USER'])+                self.assertEqual('private', settings['MB_PASSWORD'])+                self.assertEqual('fixture', settings['MB_AUTH_HOST'])+                self.assertEqual(arguments, sys.argv)+                return 0+            with patch('sys.argv', arguments), patch.dict('os.environ', {}, clear=True), \+                 patch.object(check.run, 'HERE', root), \+                 patch.object(check.run, 'sql_client', return_value='MariaDB'), \+                 patch.object(auth_fixture, 'environment', side_effect=fixture), \+                 patch.object(check, 'main', side_effect=run):+                self.assertEqual(0, check.main_with_authentication())+            self.assertEqual([], list((root/'.local').iterdir()))++    def test_created_container_is_cleaned_after_creation_error_or_interrupt(self):+        for error in [RuntimeError('created but start failed'), KeyboardInterrupt()]:+            with self.subTest(error=type(error).__name__), tempfile.TemporaryDirectory() as folder:+                owner = []+                def command(args, **_):+                    if args[:2] == ['docker', 'run']:+                        owner.append(args[args.index('--name')+1])+                        self.assertIn('mariadb-haskell.fixture-owner='+owner[0], args)+                        raise error+                def run(args, **_):+                    return subprocess.CompletedProcess(args, 0, owner[0]+'\n', '')+                with patch.object(auth_fixture, 'HERE', Path(folder)), \+                     patch.object(auth_fixture, 'command', side_effect=command), \+                     patch.object(auth_fixture.subprocess, 'run', side_effect=run) as cleanup:+                    with self.assertRaises(type(error)):+                        with auth_fixture.environment():+                            self.fail('failed fixture yielded an environment')+                removals = [c.args[0] for c in cleanup.call_args_list if c.args[0][:3] == ['docker', 'rm', '-f']]+                self.assertEqual([['docker', 'rm', '-f', owner[0]]], removals)++    def test_fixture_log_failure_cannot_skip_removal(self):+        def run(args, **_):+            if args[:2] == ['docker', 'logs']:+                raise OSError('diagnostics failed')+            return subprocess.CompletedProcess(args, 0, 'owned\n', '')+        with patch.object(auth_fixture.subprocess, 'run', side_effect=run) as cleanup:+            auth_fixture.cleanup_container('owned')+        self.assertEqual(['docker', 'rm', '-f', 'owned'], cleanup.call_args_list[-1].args[0])++    def test_name_collision_or_absent_container_is_not_removed(self):+        for result in [subprocess.CompletedProcess([], 0, 'another-owner\n', ''),+                       subprocess.CompletedProcess([], 1, '', 'absent')]:+            with self.subTest(result=result), patch.object(auth_fixture.subprocess, 'run', return_value=result) as cleanup:+                auth_fixture.cleanup_container('owned')+                self.assertEqual(1, cleanup.call_count)++    def test_runtime_identity_ignores_provenance_indexes(self):+        image = {'Id': 'old-index', 'RepoDigests': ['old-attestation'],+                 'Architecture': 'arm64', 'Os': 'linux',+                 'RootFS': {'Type': 'layers', 'Layers': ['content']},+                 'Config': {'Env': ['SETTING=1'], 'Entrypoint': ['server']}}+        later = {**image, 'Id': 'new-index', 'RepoDigests': ['new-attestation']}+        self.assertEqual(auth_fixture.runtime_identity(image), auth_fixture.runtime_identity(later))++    def test_runtime_identity_detects_files_and_configuration_changes(self):+        image = {'RootFS': {'Layers': ['old']}, 'Config': {'Env': ['SETTING=1']}}+        for changed in [{**image, 'RootFS': {'Layers': ['new']}},+                        {**image, 'Config': {'Env': ['SETTING=2']}}]:+            self.assertNotEqual(auth_fixture.runtime_identity(image), auth_fixture.runtime_identity(changed))++    def test_exact_initial_runtime_alias_is_verified(self):+        aliases = auth.legacy_runtime_aliases()['MB_AUTH_FIXTURE_IMAGE_ID']+        self.assertEqual(1, len(aliases))+        self.assertTrue(next(iter(aliases.values())).startswith('docker-runtime-sha256:'))++    def test_fixture_and_baseline_artifacts_are_fingerprinted(self):+        for name in ['validation/auth_fixture/Dockerfile', 'validation/auth_fixture/.dockerignore',+                     'cbits/gssapi.c', 'validation/results/stage7-reference.json',+                     'validation/results/references/stage6-publication-1.json']:+            self.assertTrue(live.is_input(name), name)+        for name in ['results.json', 'SUMMARY.md', 'SHA256SUMS']:+            self.assertTrue(live.is_input('validation/baselines/macos-arm64-external-authentication/'+name))++    def test_missing_authentication_measurements_block_publication(self):+        inputs = {'files': {}, 'sha256': 'source', 'dirty_inputs': [], 'commit': 'code'}+        attempt = {'state': 'COMPLETED', 'inputs': inputs}+        data = {'complete_suite': True, 'repetitions': 7, 'source_unchanged_during_run': True,+                'correctness': [{'id': 'stage7.required', 'status': 'PASS'}],+                'build': {'git_commit': 'code'}}+        with self.assertRaisesRegex(ValueError, 'External authentication'):+            live.verify({'latest_attempt': attempt,+                         'latest_completed_run': {'attempt': attempt, 'results': data}}, inputs)++    def test_fixture_image_changes_withhold_timing_deltas(self):+        spec = auth.specification()+        data = {'dependencies': [], 'server': {}, 'machine': {}, 'haskell_benchmark_rts': [],+                'build': {'ghc': 'compiler', 'cabal': 'tool'},+                'inputs': {'files': dict.fromkeys(spec.workload_paths, 'same')},+                'profiles': {}, 'environment_profile': {'MB_AUTH_FIXTURE_IMAGE_ID': 'new-image'}}+        baseline = copy.deepcopy(data)+        baseline['environment_profile']['MB_AUTH_FIXTURE_IMAGE_ID'] = 'old-image'+        with patch.object(features, 'load_baseline', return_value=(baseline, 'hash')):+            result = features.baseline_comparison(spec, data)+        self.assertFalse(result['comparable'])+        self.assertEqual({}, result['profiles'])++    def test_contract_correction_is_not_a_driver_improvement(self):+        baseline = json.loads(live.BASELINE.read_text())+        data = copy.deepcopy(baseline)+        for case in data['correctness']:+            if case['id'] in ['reliability.timeout-before-metadata', 'auth.pam-environment']:+                case['status'] = 'PASS'+        data['comparison'] = {'regressions': [], 'improved_cases':+            ['reliability.timeout-before-metadata', 'auth.pam-environment']}+        attempt = {'state': 'COMPLETED', 'exit_code': 0, 'started_at': 'start',+            'finished_at': 'finish', 'run_directory': 'logs', 'inputs': {+                'commit': data['build']['git_commit'], 'branch': 'main', 'sha256': 'source',+                'dirty_inputs': [], 'files': {'validation/hs/Validation/Stage7.hs': 'source'}}}+        text = live.render({'latest_attempt': attempt,+            'latest_completed_run': {'attempt': attempt, 'results': data}}, baseline)+        self.assertIn('Approved test-contract correction', text)+        self.assertNotIn('- `reliability.timeout-before-metadata`', text)+        self.assertIn('- `auth.pam-environment`', text)+        self.assertIn('1 newly passing check', text)+        self.assertEqual('FAIL', next(c['status'] for c in baseline['correctness']+            if c['id'] == 'reliability.timeout-before-metadata'))++    def candidate(self):+        prior, _ = stage1_gates.reference(live.ROOT/'validation/results/stage7-reference.json')+        candidate = copy.deepcopy(prior)+        for case in candidate['correctness']:+            if case['id'] in ['reliability.timeout-before-metadata', 'auth.pam-environment', 'auth.gssapi-environment']:+                case['status'] = 'PASS'+        candidate['correctness'].append({'id': 'stage7.required', 'status': 'PASS'})+        return prior, candidate++    def test_every_published_stage6_pass_is_required(self):+        prior, candidate = self.candidate()+        case = next(c for c in candidate['correctness'] if c['id'].startswith('stage6.'))+        case['status'] = 'FAIL'+        result = stage1_gates.compare(candidate, prior, prior, require_targets=False)+        self.assertIn('Stage 6: lost PASS '+case['id'], result['correctness_failures'])++    def test_external_authentication_cannot_stay_unavailable(self):+        for ident in ['auth.pam-environment', 'auth.gssapi-environment']:+            prior, candidate = self.candidate()+            next(c for c in candidate['correctness'] if c['id'] == ident)['status'] = 'ENVIRONMENT_UNAVAILABLE'+            result = stage1_gates.compare(candidate, prior, prior, require_targets=False)+            self.assertIn('required external-auth/timeout target not passing: '+ident,+                          result['correctness_failures'])++    def test_fixture_preflight_failure_remains_visible(self):+        with tempfile.TemporaryDirectory() as folder:+            config = Path(folder)/'config.json'+            config.write_text('{}')+            with patch('sys.argv', ['check.py', '--config', str(config), '--working-tree']), \+                 patch.dict('os.environ', {}, clear=True), \+                 patch.object(check.run, 'sql_client', return_value='MariaDB'), \+                 patch('auth_fixture.environment', side_effect=RuntimeError('fixture startup failed')), \+                 patch.object(check.live, 'publish') as publish, \+                 patch.object(check.live, 'input_snapshot', return_value={'files': {}}):+                self.assertEqual(1, check.main_with_authentication())+            attempt = publish.call_args.args[0]+            self.assertEqual('FAILED', attempt['state'])+            self.assertIn('fixture startup failed', attempt['reason'])+            self.assertTrue(attempt['uncommitted_review'])+++if __name__ == '__main__':+    unittest.main()
+ word24-bench/Benchmark.hs view
@@ -0,0 +1,32 @@+module Main where++import Criterion.Main+import Control.DeepSeq+import Data.Int.Int24+import Data.Word.Word24+import Data.Int+import Data.Word+import Data.List++main = defaultMain+  [ bgroup "Int24" intses+  , bgroup "Word24" wordses+  , bgroup "Int16"    baseses+  , bgroup "Word16" basessW+  ]++benches :: (Enum i, Num i, NFData i, Integral i) => i -> [Benchmark]+benches x =+   [ bench "Add" $ nf (\i -> foldl' (+) i [1..100]) x+   , bench "Mul" $ nf (\i -> foldl' (*) i [1..100]) x+   , bench "quot" $ nf (\i -> map (flip quot i) [1..25]) x+   , bench "rem" $ nf (\i -> map (flip rem i) [1..25]) x+   , bench "div" $ nf (\i -> map (flip div i) [1..25]) x+   , bench "mod" $ nf (\i -> map (flip mod i) [1..25]) x+   ]+{-# INLINE benches #-}++intses  = benches (1 :: Int24)+wordses = benches (1 :: Word24)+baseses = benches (1 :: Int16)+basessW = benches (1 :: Word16)