├── .envrc ├── .gitattributes ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── workflows │ └── ci.yaml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── SECURITY.md ├── connection-string-wasm ├── Cargo.lock ├── Cargo.toml ├── README.md ├── package.json └── src │ ├── lib.rs │ ├── wasm.rs │ └── wasm │ ├── ado.rs │ └── jdbc.rs ├── flake.lock ├── flake.nix ├── rust-toolchain.toml ├── src ├── ado.rs ├── error.rs ├── jdbc.rs ├── lib.rs └── utils.rs └── tests └── test.rs /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, 10 | education, socio-economic status, nationality, personal appearance, race, 11 | religion, or sexual identity and orientation. 12 | 13 | ## Our Standards 14 | 15 | Examples of behavior that contributes to creating a positive environment 16 | include: 17 | 18 | - Using welcoming and inclusive language 19 | - Being respectful of differing viewpoints and experiences 20 | - Gracefully accepting constructive criticism 21 | - Focusing on what is best for the community 22 | - Showing empathy towards other community members 23 | 24 | Examples of unacceptable behavior by participants include: 25 | 26 | - The use of sexualized language or imagery and unwelcome sexual attention or 27 | advances 28 | - Trolling, insulting/derogatory comments, and personal or political attacks 29 | - Public or private harassment 30 | - Publishing others' private information, such as a physical or electronic 31 | address, without explicit permission 32 | - Other conduct which could reasonably be considered inappropriate in a 33 | professional setting 34 | 35 | 36 | ## Our Responsibilities 37 | 38 | Project maintainers are responsible for clarifying the standards of acceptable 39 | behavior and are expected to take appropriate and fair corrective action in 40 | response to any instances of unacceptable behavior. 41 | 42 | Project maintainers have the right and responsibility to remove, edit, or 43 | reject comments, commits, code, wiki edits, issues, and other contributions 44 | that are not aligned to this Code of Conduct, or to ban temporarily or 45 | permanently any contributor for other behaviors that they deem inappropriate, 46 | threatening, offensive, or harmful. 47 | 48 | ## Scope 49 | 50 | This Code of Conduct applies both within project spaces and in public spaces 51 | when an individual is representing the project or its community. Examples of 52 | representing a project or community include using an official project e-mail 53 | address, posting via an official social media account, or acting as an appointed 54 | representative at an online or offline event. Representation of a project may be 55 | further defined and clarified by project maintainers. 56 | 57 | ## Enforcement 58 | 59 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 60 | reported by contacting the project team at yoshuawuyts@gmail.com, or through 61 | IRC. All complaints will be reviewed and investigated and will result in a 62 | response that is deemed necessary and appropriate to the circumstances. The 63 | project team is obligated to maintain confidentiality with regard to the 64 | reporter of an incident. 65 | Further details of specific enforcement policies may be posted separately. 66 | 67 | Project maintainers who do not follow or enforce the Code of Conduct in good 68 | faith may face temporary or permanent repercussions as determined by other 69 | members of the project's leadership. 70 | 71 | ## Attribution 72 | 73 | This Code of Conduct is adapted from the Contributor Covenant, version 1.4, 74 | available at 75 | https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 76 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Contributions include code, documentation, answering user questions, running the 3 | project's infrastructure, and advocating for all types of users. 4 | 5 | The project welcomes all contributions from anyone willing to work in good faith 6 | with other contributors and the community. No contribution is too small and all 7 | contributions are valued. 8 | 9 | This guide explains the process for contributing to the project's GitHub 10 | Repository. 11 | 12 | - [Code of Conduct](#code-of-conduct) 13 | - [Bad Actors](#bad-actors) 14 | 15 | ## Code of Conduct 16 | The project has a [Code of Conduct](./CODE_OF_CONDUCT.md) that *all* 17 | contributors are expected to follow. This code describes the *minimum* behavior 18 | expectations for all contributors. 19 | 20 | As a contributor, how you choose to act and interact towards your 21 | fellow contributors, as well as to the community, will reflect back not only 22 | on yourself but on the project as a whole. The Code of Conduct is designed and 23 | intended, above all else, to help establish a culture within the project that 24 | allows anyone and everyone who wants to contribute to feel safe doing so. 25 | 26 | Should any individual act in any way that is considered in violation of the 27 | [Code of Conduct](./CODE_OF_CONDUCT.md), corrective actions will be taken. It is 28 | possible, however, for any individual to *act* in such a manner that is not in 29 | violation of the strict letter of the Code of Conduct guidelines while still 30 | going completely against the spirit of what that Code is intended to accomplish. 31 | 32 | Open, diverse, and inclusive communities live and die on the basis of trust. 33 | Contributors can disagree with one another so long as they trust that those 34 | disagreements are in good faith and everyone is working towards a common 35 | goal. 36 | 37 | ## Bad Actors 38 | All contributors to tacitly agree to abide by both the letter and 39 | spirit of the [Code of Conduct](./CODE_OF_CONDUCT.md). Failure, or 40 | unwillingness, to do so will result in contributions being respectfully 41 | declined. 42 | 43 | A *bad actor* is someone who repeatedly violates the *spirit* of the Code of 44 | Conduct through consistent failure to self-regulate the way in which they 45 | interact with other contributors in the project. In doing so, bad actors 46 | alienate other contributors, discourage collaboration, and generally reflect 47 | poorly on the project as a whole. 48 | 49 | Being a bad actor may be intentional or unintentional. Typically, unintentional 50 | bad behavior can be easily corrected by being quick to apologize and correct 51 | course *even if you are not entirely convinced you need to*. Giving other 52 | contributors the benefit of the doubt and having a sincere willingness to admit 53 | that you *might* be wrong is critical for any successful open collaboration. 54 | 55 | Don't be a bad actor. 56 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | env: 10 | RUSTFLAGS: -Dwarnings 11 | 12 | jobs: 13 | build_and_test_linux: 14 | name: Build and test on Linux 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest] 19 | 20 | steps: 21 | - uses: actions/checkout@main 22 | 23 | - uses: cachix/install-nix-action@v20 24 | 25 | - name: Build WASM Library 26 | run: nix build 27 | 28 | - name: Run Tests 29 | run: nix run .#test 30 | 31 | build_and_test_macos: 32 | name: Build and test on macOS 33 | runs-on: ${{ matrix.os }} 34 | strategy: 35 | matrix: 36 | os: [macos-latest] 37 | 38 | steps: 39 | - uses: actions/checkout@main 40 | 41 | - uses: cachix/install-nix-action@v20 42 | 43 | - name: Build WASM Library 44 | run: nix build 45 | 46 | - name: Run Tests 47 | run: nix run .#test 48 | 49 | clippy: 50 | runs-on: ubuntu-latest 51 | env: 52 | RUSTFLAGS: "-Dwarnings" 53 | steps: 54 | - uses: actions/checkout@v2 55 | - uses: actions-rs/toolchain@v1 56 | with: 57 | toolchain: stable 58 | components: clippy 59 | override: true 60 | - uses: actions-rs/clippy-check@v1 61 | with: 62 | token: ${{ secrets.GITHUB_TOKEN }} 63 | args: --all-features 64 | 65 | format: 66 | runs-on: ubuntu-latest 67 | steps: 68 | - uses: actions/checkout@v2 69 | - uses: actions-rs/toolchain@v1 70 | with: 71 | toolchain: stable 72 | components: rustfmt 73 | override: true 74 | - name: Check formatting 75 | run: cargo fmt -- --check 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | tmp/ 3 | .DS_Store 4 | result 5 | .direnv 6 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "connection-string" 7 | version = "0.2.0" 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "connection-string" 3 | version = "0.2.0" 4 | license = "MIT OR Apache-2.0" 5 | repository = "https://github.com/prisma/connection-string" 6 | documentation = "https://docs.rs/connection-string" 7 | description = "Connection string parsing in Rust (and WebAssembly)" 8 | readme = "README.md" 9 | edition = "2021" 10 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2020 Yoshua Wuyts 179 | 180 | Licensed under the Apache License, Version 2.0 (the "License"); 181 | you may not use this file except in compliance with the License. 182 | You may obtain a copy of the License at 183 | 184 | http://www.apache.org/licenses/LICENSE-2.0 185 | 186 | Unless required by applicable law or agreed to in writing, software 187 | distributed under the License is distributed on an "AS IS" BASIS, 188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 189 | See the License for the specific language governing permissions and 190 | limitations under the License. 191 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Yoshua Wuyts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

connection-string

2 |
3 | 4 | ADO.net and JDBC connection string parsing in Rust and JavaScript 5 | 6 |
7 | 8 |
9 | 10 |
11 | 12 | 13 | Crates.io version 15 | 16 | 17 | 18 | Download 20 | 21 | 22 | 23 | docs.rs docs 25 | 26 | 27 | 28 | npm version 30 | 31 |
32 | 33 |
34 |

35 | 36 | API Docs 37 | 38 | | 39 | 40 | Releases 41 | 42 | | 43 | 44 | Contributing 45 | 46 |

47 |
48 | 49 | ## Installation for Rust 50 | ```sh 51 | $ cargo add connection-string 52 | ``` 53 | 54 | ## Usage for JavaScript 55 | The crate is available in npm as `@pimeys/connection-string`. Usage patters try 56 | to follow the Rust version as close as possible. Please see the [Rust 57 | docs](https://docs.rs/connection-string) for more information. 58 | 59 | JDBC: 60 | 61 | ``` javascript 62 | const j = new JdbcString("jdbc:sqlserver://localhost\\INSTANCE:1433;database=master;user=SA;password={my_password;123}"); 63 | 64 | console.log(j.server_name()); // "localhost" 65 | console.log(j.port()); // 1433 66 | console.log(j.instance_name()); // "INSTANCE" 67 | console.log(j.get("database")); // "master" 68 | console.log(j.get("password")); // "my_password;123" (see escaping) 69 | console.log(j.keys()); // ["database", "user", "password"] 70 | console.log(j.set("password", "a;;new;;password")); // "my_password;123" (returns the old value, if available) 71 | 72 | // "jdbc:sqlserver://localhost\INSTANCE:1433;user=SA;database=master;password=a{;;}new{;;}password" 73 | console.log(j.to_string()) 74 | ``` 75 | 76 | ADO.net: 77 | 78 | ``` javascript 79 | const a = new AdoNetString("server=tcp:localhost,1433;user=SA;password=a{;;}new{;;}password"); 80 | 81 | console.log(a.get("password")); // a;;new;;password 82 | console.log(a.set("user", "john")); // "SA" (returns the old value, if available) 83 | 84 | // "server=tcp:localhost,1433;user=john;password=a{;;}new{;;}password" 85 | console.log(j.to_string()) 86 | ``` 87 | 88 | ## Safety 89 | This crate uses ``#![deny(unsafe_code)]`` to ensure everything is implemented in 90 | 100% Safe Rust. 91 | 92 | ## Contributing 93 | Want to join us? Check out our ["Contributing" guide][contributing] and take a 94 | look at some of these issues: 95 | 96 | - [Issues labeled "good first issue"][good-first-issue] 97 | - [Issues labeled "help wanted"][help-wanted] 98 | 99 | [contributing]: https://github.com/prisma/connection-string/blob/master.github/CONTRIBUTING.md 100 | [good-first-issue]: https://github.com/prisma/connection-string/labels/good%20first%20issue 101 | [help-wanted]: https://github.com/prisma/connection-string/labels/help%20wanted 102 | 103 | ## Building 104 | 105 | The build procedure and dependencies are defined in the provided 106 | [flake.nix](flake.nix) file. Please install the unstable Nix with flakes support 107 | ([Linux](https://nixos.wiki/wiki/Nix_Installation_Guide), [macOS](https://gist.github.com/sagittaros/32dc6ffcbc423dc0fed7eef24698d5ca)). 108 | 109 | The WASM module can be built with: 110 | 111 | ```bash 112 | nix build 113 | ``` 114 | 115 | This creates a link `result` to the current directory, containing a NodeJS 116 | package with the Rust code compiled as WASM bytecode. 117 | 118 | ## Testing 119 | 120 | Run the tests with the nix subcommand: 121 | 122 | ```bash 123 | nix run .#test 124 | ``` 125 | 126 | ## Publishing 127 | 128 | The `updatePackageVersion` command changes the package version to the Rust `Cargo.toml` and 129 | JavaScript `package.json` at the same time: 130 | 131 | ```bash 132 | nix run .#updatePackageVersion 0.1.14 133 | ``` 134 | 135 | Don't forget to add the tag before publishing the library: 136 | 137 | ```bash 138 | git tag v0.1.14 139 | ``` 140 | 141 | The publishing can be done separately or together with the `publish` command: 142 | 143 | ```bash 144 | nix run .#publishRust 145 | nix run .#publishJavascript 146 | ``` 147 | 148 | or 149 | 150 | ```bash 151 | nix run .#publish 152 | ``` 153 | 154 | Please be sure you have the corresponding publishing rights in crates and npmjs. 155 | 156 | ## License 157 | 158 | 159 | Licensed under either of Apache License, Version 160 | 2.0 or MIT license at your option. 161 | 162 | 163 |
164 | 165 | 166 | Unless you explicitly state otherwise, any contribution intentionally submitted 167 | for inclusion in this crate by you, as defined in the Apache-2.0 license, shall 168 | be dual licensed as above, without any additional terms or conditions. 169 | 170 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | If you have a security issue to report, please contact us at [security@prisma.io](mailto:security@prisma.io). 4 | -------------------------------------------------------------------------------- /connection-string-wasm/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "bumpalo" 7 | version = "3.12.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" 10 | 11 | [[package]] 12 | name = "cfg-if" 13 | version = "1.0.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 16 | 17 | [[package]] 18 | name = "connection-string" 19 | version = "0.2.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "510ca239cf13b7f8d16a2b48f263de7b4f8c566f0af58d901031473c76afb1e3" 22 | 23 | [[package]] 24 | name = "connection-string-wasm" 25 | version = "0.2.0" 26 | dependencies = [ 27 | "connection-string", 28 | "js-sys", 29 | "wasm-bindgen", 30 | ] 31 | 32 | [[package]] 33 | name = "js-sys" 34 | version = "0.3.56" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" 37 | dependencies = [ 38 | "wasm-bindgen", 39 | ] 40 | 41 | [[package]] 42 | name = "lazy_static" 43 | version = "1.4.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 46 | 47 | [[package]] 48 | name = "log" 49 | version = "0.4.17" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 52 | dependencies = [ 53 | "cfg-if", 54 | ] 55 | 56 | [[package]] 57 | name = "proc-macro2" 58 | version = "1.0.54" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "e472a104799c74b514a57226160104aa483546de37e839ec50e3c2e41dd87534" 61 | dependencies = [ 62 | "unicode-ident", 63 | ] 64 | 65 | [[package]] 66 | name = "quote" 67 | version = "1.0.26" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" 70 | dependencies = [ 71 | "proc-macro2", 72 | ] 73 | 74 | [[package]] 75 | name = "syn" 76 | version = "1.0.109" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 79 | dependencies = [ 80 | "proc-macro2", 81 | "quote", 82 | "unicode-ident", 83 | ] 84 | 85 | [[package]] 86 | name = "unicode-ident" 87 | version = "1.0.8" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 90 | 91 | [[package]] 92 | name = "wasm-bindgen" 93 | version = "0.2.79" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" 96 | dependencies = [ 97 | "cfg-if", 98 | "wasm-bindgen-macro", 99 | ] 100 | 101 | [[package]] 102 | name = "wasm-bindgen-backend" 103 | version = "0.2.79" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" 106 | dependencies = [ 107 | "bumpalo", 108 | "lazy_static", 109 | "log", 110 | "proc-macro2", 111 | "quote", 112 | "syn", 113 | "wasm-bindgen-shared", 114 | ] 115 | 116 | [[package]] 117 | name = "wasm-bindgen-macro" 118 | version = "0.2.79" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" 121 | dependencies = [ 122 | "quote", 123 | "wasm-bindgen-macro-support", 124 | ] 125 | 126 | [[package]] 127 | name = "wasm-bindgen-macro-support" 128 | version = "0.2.79" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" 131 | dependencies = [ 132 | "proc-macro2", 133 | "quote", 134 | "syn", 135 | "wasm-bindgen-backend", 136 | "wasm-bindgen-shared", 137 | ] 138 | 139 | [[package]] 140 | name = "wasm-bindgen-shared" 141 | version = "0.2.79" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" 144 | -------------------------------------------------------------------------------- /connection-string-wasm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "connection-string-wasm" 3 | version = "0.2.0" 4 | license = "MIT OR Apache-2.0" 5 | repository = "https://github.com/prisma/connection-string" 6 | documentation = "https://docs.rs/connection-string" 7 | description = "Connection string parsing in Rust (and WebAssembly)" 8 | readme = "README.md" 9 | edition = "2021" 10 | 11 | [package.metadata.wasm-pack.profile.release] 12 | wasm-opt = ["-Oz", "--enable-mutable-globals"] 13 | 14 | [target.'cfg(target_arch = "wasm32")'.dependencies] 15 | wasm-bindgen = "=0.2.79" 16 | js-sys = "0.3.56" 17 | connection-string = "0.2" 18 | 19 | [dev-dependencies] 20 | 21 | [lib] 22 | crate-type = ["cdylib", "lib"] 23 | 24 | [profile.release] 25 | lto = true 26 | -------------------------------------------------------------------------------- /connection-string-wasm/README.md: -------------------------------------------------------------------------------- 1 |

connection-string-wasm

2 |
3 | 4 | Wrapper around [connection-string](../README.md) for web-assembly 5 | 6 |
7 |
8 | 9 | ## License 10 | 11 | 12 | Licensed under either of Apache License, Version 13 | 2.0 or MIT license at your option. 14 | 15 | 16 |
17 | 18 | 19 | Unless you explicitly state otherwise, any contribution intentionally submitted 20 | for inclusion in this crate by you, as defined in the Apache-2.0 license, shall 21 | be dual licensed as above, without any additional terms or conditions. 22 | 23 | -------------------------------------------------------------------------------- /connection-string-wasm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pimeys/connection-string", 3 | "version": "0.1.14", 4 | "description": "A parser for ADO.net and JDBC connection strings", 5 | "main": "src/connection_string.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/prisma/connection-string" 12 | }, 13 | "author": "Prisma", 14 | "license": "MIT OR Apache-2.0", 15 | "homepage": "https://github.com/prisma/connection-string" 16 | } 17 | -------------------------------------------------------------------------------- /connection-string-wasm/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod wasm; 2 | -------------------------------------------------------------------------------- /connection-string-wasm/src/wasm.rs: -------------------------------------------------------------------------------- 1 | /// The WASM ADO.net connection string parsing. 2 | mod ado; 3 | /// The WASM JDBC connection string parsing. 4 | mod jdbc; 5 | -------------------------------------------------------------------------------- /connection-string-wasm/src/wasm/ado.rs: -------------------------------------------------------------------------------- 1 | use connection_string::AdoNetString as BaseAdoNetString; 2 | use wasm_bindgen::prelude::*; 3 | 4 | #[wasm_bindgen] 5 | #[derive(Debug)] 6 | /// A version of `JdbcString` to be used from web-assembly. 7 | pub struct AdoNetString { 8 | inner: BaseAdoNetString, 9 | } 10 | 11 | #[wasm_bindgen] 12 | impl AdoNetString { 13 | #[wasm_bindgen(constructor)] 14 | /// A constructor to create a new `AdoNet`, used from JavaScript with 15 | /// `new AdoNet("server=tcp:localhost,1433")`. 16 | pub fn new(s: &str) -> Result { 17 | let inner = s 18 | .parse() 19 | .map_err(|err| JsValue::from_str(&format!("{}", err)))?; 20 | 21 | Ok(Self { inner }) 22 | } 23 | 24 | /// Get a parameter from the connection's key-value pairs 25 | pub fn get(&self, key: &str) -> Option { 26 | self.inner.get(key).map(|s| s.to_string()) 27 | } 28 | 29 | /// Set a parameter value to the connection's key-value pairs. If replacing 30 | /// a pre-existing value, returns the old value. 31 | pub fn set(&mut self, key: &str, value: &str) -> Option { 32 | self.inner.insert(key.into(), value.into()) 33 | } 34 | 35 | /// Get a string representation of the `AdoNetString`. 36 | pub fn to_string(&self) -> String { 37 | format!("{}", self.inner) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /connection-string-wasm/src/wasm/jdbc.rs: -------------------------------------------------------------------------------- 1 | use connection_string::JdbcString as BaseJdbcString; 2 | use js_sys::Array; 3 | use wasm_bindgen::prelude::*; 4 | 5 | #[wasm_bindgen] 6 | #[derive(Debug)] 7 | /// A version of `JdbcString` to be used from web-assembly. 8 | pub struct JdbcString { 9 | inner: BaseJdbcString, 10 | } 11 | 12 | #[wasm_bindgen] 13 | impl JdbcString { 14 | #[wasm_bindgen(constructor)] 15 | /// A constructor to create a new `JdbcInstance`, used from JavaScript with 16 | /// `new JdbcString("sqlserver://...")`. 17 | pub fn new(s: &str) -> Result { 18 | let inner = if s.starts_with("jdbc") { 19 | s.parse() 20 | } else { 21 | format!("jdbc:{}", s).parse() 22 | } 23 | .map_err(|err| JsValue::from_str(&format!("{}", err)))?; 24 | 25 | Ok(Self { inner }) 26 | } 27 | 28 | /// Access the connection sub-protocol 29 | pub fn sub_protocol(&self) -> String { 30 | self.inner.sub_protocol().to_string() 31 | } 32 | 33 | /// Access the connection server name 34 | pub fn server_name(&self) -> Option { 35 | self.inner.server_name().map(|s| s.to_string()) 36 | } 37 | 38 | /// Access the connection's instance name 39 | pub fn instance_name(&self) -> Option { 40 | self.inner.instance_name().map(|s| s.to_string()) 41 | } 42 | 43 | /// Access the connection's port 44 | pub fn port(&self) -> Option { 45 | self.inner.port() 46 | } 47 | 48 | /// Get all keys from the connection's key-value pairs 49 | pub fn keys(&self) -> Array { 50 | self.inner 51 | .keys() 52 | .map(|k| JsValue::from(k)) 53 | .collect::() 54 | } 55 | 56 | /// Get a parameter from the connection's key-value pairs 57 | pub fn get(&self, key: &str) -> Option { 58 | self.inner.properties().get(key).map(|s| s.to_string()) 59 | } 60 | 61 | /// Set a parameter value to the connection's key-value pairs. If replacing 62 | /// a pre-existing value, returns the old value. 63 | pub fn set(&mut self, key: &str, value: &str) -> Option { 64 | self.inner.properties_mut().insert(key.into(), value.into()) 65 | } 66 | 67 | /// Get a string representation of the `JdbcString`. 68 | pub fn to_string(&self) -> String { 69 | format!("{}", self.inner) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1642700792, 6 | "narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "846b2ae0fc4cc943637d3d1def4454213e203cba", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "nixpkgs": { 19 | "locked": { 20 | "lastModified": 1643805626, 21 | "narHash": "sha256-AXLDVMG+UaAGsGSpOtQHPIKB+IZ0KSd9WS77aanGzgc=", 22 | "owner": "NixOS", 23 | "repo": "nixpkgs", 24 | "rev": "554d2d8aa25b6e583575459c297ec23750adb6cb", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "owner": "NixOS", 29 | "ref": "nixos-unstable", 30 | "repo": "nixpkgs", 31 | "type": "github" 32 | } 33 | }, 34 | "root": { 35 | "inputs": { 36 | "flake-utils": "flake-utils", 37 | "nixpkgs": "nixpkgs", 38 | "rust-overlay": "rust-overlay" 39 | } 40 | }, 41 | "rust-overlay": { 42 | "inputs": { 43 | "flake-utils": [ 44 | "flake-utils" 45 | ], 46 | "nixpkgs": [ 47 | "nixpkgs" 48 | ] 49 | }, 50 | "locked": { 51 | "lastModified": 1643941258, 52 | "narHash": "sha256-uHyEuICSu8qQp6adPTqV33ajiwoF0sCh+Iazaz5r7fo=", 53 | "owner": "oxalica", 54 | "repo": "rust-overlay", 55 | "rev": "674156c4c2f46dd6a6846466cb8f9fee84c211ca", 56 | "type": "github" 57 | }, 58 | "original": { 59 | "owner": "oxalica", 60 | "repo": "rust-overlay", 61 | "type": "github" 62 | } 63 | } 64 | }, 65 | "root": "root", 66 | "version": 7 67 | } 68 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "ADO.net and JDBC Connection String Parser."; 3 | 4 | inputs = { 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 7 | rust-overlay = { 8 | url = "github:oxalica/rust-overlay"; 9 | inputs.nixpkgs.follows = "nixpkgs"; 10 | inputs.flake-utils.follows = "flake-utils"; 11 | }; 12 | }; 13 | 14 | outputs = { self, nixpkgs, flake-utils, rust-overlay }: 15 | flake-utils.lib.eachDefaultSystem (system: let 16 | overlays = [ (import rust-overlay) ]; 17 | pkgs = import nixpkgs { inherit system overlays; }; 18 | rust = pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; 19 | nix = pkgs.nixFlakes; 20 | inherit (pkgs) wasm-bindgen-cli rustPlatform nodejs; 21 | in { 22 | defaultPackage = rustPlatform.buildRustPackage { 23 | name = "connection-string-wasm"; 24 | src = builtins.path { path = ./connection-string-wasm; name = "connection-string-wasm"; }; 25 | 26 | cargoLock = { 27 | lockFile = ./connection-string-wasm/Cargo.lock; 28 | }; 29 | 30 | nativeBuildInputs = [ rust wasm-bindgen-cli ]; 31 | 32 | buildPhase = '' 33 | export RUSTFLAGS="-Dwarnings" 34 | export RUST_BACKTRACE=1 35 | 36 | cargo build --release --target=wasm32-unknown-unknown 37 | echo 'Creating out dir...' 38 | mkdir -p $out/src; 39 | echo 'Copying package.json...' 40 | cp ./package.json $out/; 41 | echo 'Copying README.md...' 42 | cp README.md $out/; 43 | echo 'Generating node module...' 44 | wasm-bindgen \ 45 | --target nodejs \ 46 | --out-dir $out/src \ 47 | target/wasm32-unknown-unknown/release/connection_string_wasm.wasm; 48 | ''; 49 | checkPhase = "echo 'Check phase: skipped'"; 50 | installPhase = "echo 'Install phase: skipped'"; 51 | }; 52 | 53 | packages = { 54 | cargo = { 55 | type = "app"; 56 | program = "${rust}/bin/cargo"; 57 | }; 58 | 59 | # Takes the new package version as first and only argument, and updates package.json 60 | updatePackageVersion = pkgs.writeShellScriptBin "updateNpmPackageVersion" '' 61 | ${pkgs.jq}/bin/jq ".version = \"$1\"" package.json > /tmp/package.json 62 | rm package.json 63 | cp /tmp/package.json package.json 64 | sed -i "s/^version\ =.*$/version = \"$1\"/" Cargo.toml 65 | ''; 66 | test = pkgs.writeShellScriptBin "test" '' 67 | export RUSTFLAGS="-Dwarnings" 68 | export RUST_BACKTRACE=1 69 | 70 | ${rust}/bin/cargo test 71 | ''; 72 | publishRust = pkgs.writeShellScriptBin "publishRust" '' 73 | ${rust}/bin/cargo publish 74 | ''; 75 | publishJavascript = pkgs.writeShellScriptBin "publishRust" '' 76 | ${nix}/bin/nix build 77 | ${nodejs}/bin/npm publish ./result --access public --tag latest 78 | ''; 79 | publish = pkgs.writeShellScriptBin "publish" '' 80 | ${nix}/bin/nix publishRust 81 | ${nix}/bin/nix publishJavascript 82 | ''; 83 | npm = { 84 | type = "app"; 85 | program = "${nodejs}/bin/npm"; 86 | }; 87 | wasm-bindgen = { 88 | type = "app"; 89 | program = "${wasm-bindgen-cli}/bin/wasm-bindgen"; 90 | }; 91 | syncWasmBindgenVersions = pkgs.writeShellScriptBin "updateWasmBindgenVersion" '' 92 | echo 'Syncing wasm-bindgen version in crate with that of the installed CLI...' 93 | sed -i "s/^wasm-bindgen\ =.*$/wasm-bindgen = \"=${wasm-bindgen-cli.version}\"/" Cargo.toml 94 | ''; 95 | }; 96 | devShell = pkgs.mkShell { 97 | nativeBuildInputs = [ pkgs.bashInteractive ]; 98 | buildInputs = [ 99 | rust 100 | pkgs.nodejs 101 | pkgs.wasm-bindgen-cli 102 | ]; 103 | }; 104 | }); 105 | } 106 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = [] 4 | targets = [ "wasm32-unknown-unknown" ] 5 | profile = "default" 6 | -------------------------------------------------------------------------------- /src/ado.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Deref, DerefMut}; 2 | use std::str::FromStr; 3 | use std::{collections::HashMap, fmt}; 4 | 5 | use crate::{bail, ensure}; 6 | 7 | /// An ADO.net connection string. 8 | /// 9 | /// Keywords are not case-sensitive. Values, however, may be case-sensitive, 10 | /// depending on the data source. Both keywords and values may contain whitespace 11 | /// characters. 12 | /// 13 | /// # Limitations 14 | /// 15 | /// This parser does not support [Excel connection strings with extended properties](https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-string-syntax#connecting-to-excel). 16 | /// 17 | /// [Read more](https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-string-syntax) 18 | #[derive(Debug)] 19 | pub struct AdoNetString { 20 | pairs: HashMap, 21 | } 22 | 23 | impl Deref for AdoNetString { 24 | type Target = HashMap; 25 | 26 | fn deref(&self) -> &Self::Target { 27 | &self.pairs 28 | } 29 | } 30 | 31 | impl DerefMut for AdoNetString { 32 | fn deref_mut(&mut self) -> &mut Self::Target { 33 | &mut self.pairs 34 | } 35 | } 36 | 37 | // NOTE(yosh): Unfortunately we can't parse using `split(';')` because JDBC 38 | // strings support escaping. This means that `{;}` is valid and we need to write 39 | // an actual LR parser. 40 | impl FromStr for AdoNetString { 41 | type Err = crate::Error; 42 | 43 | fn from_str(input: &str) -> Result { 44 | let mut lexer = Lexer::tokenize(input)?; 45 | let mut pairs = HashMap::new(); 46 | 47 | // Iterate over `key=value` pairs. 48 | for n in 0.. { 49 | // [property=[value][;property=value][;]] 50 | // ^ 51 | if lexer.peek().kind() == &TokenKind::Eof { 52 | break; 53 | } 54 | 55 | // [property=[value][;property=value][;]] 56 | // ^ 57 | if n != 0 { 58 | let err = "Key-value pairs must be separated by a `;`"; 59 | ensure!(lexer.next().kind() == &TokenKind::Semi, err); 60 | 61 | // [property=value[;property=value][;]] 62 | // ^ 63 | if lexer.peek().kind() == &TokenKind::Eof { 64 | break; 65 | } 66 | } 67 | 68 | // [property=[value][;property=value][;]] 69 | // ^^^^^^^^ 70 | let key = read_ident(&mut lexer)?; 71 | ensure!(!key.is_empty(), "Key must not be empty"); 72 | 73 | // [property=[value][;property=value][;]] 74 | // ^ 75 | let err = "key-value pairs must be joined by a `=`"; 76 | ensure!(lexer.next().kind() == &TokenKind::Eq, err); 77 | 78 | // [property=[value][;property=value][;]] 79 | // ^^^^^ 80 | let value = read_ident(&mut lexer)?; 81 | 82 | let key = key.to_lowercase(); 83 | pairs.insert(key, value); 84 | } 85 | Ok(Self { pairs }) 86 | } 87 | } 88 | 89 | impl fmt::Display for AdoNetString { 90 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 91 | /// Escape all non-alphanumeric characters in a string.. 92 | fn escape(s: &str) -> String { 93 | let mut output = String::with_capacity(s.len()); 94 | let mut escaping = false; 95 | for b in s.chars() { 96 | if matches!(b, ':' | '=' | '\\' | '/' | ';' | '{' | '}' | '[' | ']') { 97 | if !escaping { 98 | escaping = true; 99 | output.push('{'); 100 | } 101 | output.push(b); 102 | } else { 103 | if escaping { 104 | escaping = false; 105 | output.push('}'); 106 | } 107 | output.push(b); 108 | } 109 | } 110 | if escaping { 111 | output.push('}'); 112 | } 113 | output 114 | } 115 | 116 | let total_pairs = self.pairs.len(); 117 | 118 | for (i, (k, v)) in self.pairs.iter().enumerate() { 119 | write!(f, "{}={}", escape(k.trim()), escape(v.trim()))?; 120 | 121 | if i < total_pairs - 1 { 122 | write!(f, ";")?; 123 | } 124 | } 125 | 126 | Ok(()) 127 | } 128 | } 129 | 130 | /// Read either a valid key or value from the lexer. 131 | fn read_ident(lexer: &mut Lexer) -> crate::Result { 132 | let mut output = String::new(); 133 | loop { 134 | let Token { kind, .. } = lexer.peek(); 135 | match kind { 136 | TokenKind::Atom(c) => { 137 | let _ = lexer.next(); 138 | output.push(c); 139 | } 140 | TokenKind::Escaped(seq) => { 141 | let _ = lexer.next(); 142 | output.extend(seq); 143 | } 144 | TokenKind::Semi => break, 145 | TokenKind::Eq => break, 146 | TokenKind::Newline => { 147 | let _ = lexer.next(); 148 | continue; // NOTE(yosh): unsure if this is the correct behavior 149 | } 150 | TokenKind::Whitespace => { 151 | let _ = lexer.next(); 152 | match output.len() { 153 | 0 => continue, // ignore leading whitespace 154 | _ => output.push(' '), 155 | } 156 | } 157 | TokenKind::Eof => break, 158 | } 159 | } 160 | output = output.trim_end().to_owned(); // remove trailing whitespace 161 | Ok(output) 162 | } 163 | 164 | #[derive(Debug, Clone)] 165 | struct Token { 166 | kind: TokenKind, 167 | #[allow(dead_code)] // for future use... 168 | loc: Location, 169 | } 170 | 171 | impl Token { 172 | /// Create a new instance. 173 | fn new(kind: TokenKind, loc: Location) -> Self { 174 | Self { kind, loc } 175 | } 176 | 177 | fn kind(&self) -> &TokenKind { 178 | &self.kind 179 | } 180 | } 181 | 182 | #[derive(Debug, Clone, Eq, PartialEq)] 183 | enum TokenKind { 184 | Semi, 185 | Eq, 186 | Atom(char), 187 | Escaped(Vec), 188 | Newline, 189 | Whitespace, 190 | Eof, 191 | } 192 | 193 | #[derive(Debug)] 194 | struct Lexer { 195 | tokens: Vec, 196 | } 197 | 198 | impl Lexer { 199 | /// Parse a string into a sequence of tokens. 200 | fn tokenize(mut input: &str) -> crate::Result { 201 | let mut tokens = vec![]; 202 | let mut loc = Location::default(); 203 | while !input.is_empty() { 204 | let old_input = input; 205 | let mut chars = input.chars(); 206 | let kind = match chars.next().unwrap() { 207 | '"' => { 208 | let mut buf = Vec::new(); 209 | loop { 210 | match chars.next() { 211 | None => bail!("unclosed double quote"), 212 | // When we read a double quote inside a double quote 213 | // we need to lookahead to determine whether it's an 214 | // escape sequence or a closing delimiter. 215 | Some('"') => match lookahead(&chars) { 216 | Some('"') => { 217 | if buf.is_empty() { 218 | break; 219 | } 220 | let _ = chars.next(); 221 | buf.push('"'); 222 | buf.push('"'); 223 | } 224 | Some(_) | None => break, 225 | }, 226 | Some(c) if c.is_ascii() => buf.push(c), 227 | _ => bail!("Invalid ado.net token"), 228 | } 229 | } 230 | TokenKind::Escaped(buf) 231 | } 232 | '\'' => { 233 | let mut buf = Vec::new(); 234 | loop { 235 | match chars.next() { 236 | None => bail!("unclosed single quote"), 237 | // When we read a single quote inside a single quote 238 | // we need to lookahead to determine whether it's an 239 | // escape sequence or a closing delimiter. 240 | Some('\'') => match lookahead(&chars) { 241 | Some('\'') => { 242 | if buf.is_empty() { 243 | break; 244 | } 245 | let _ = chars.next(); 246 | buf.push('\''); 247 | buf.push('\''); 248 | } 249 | Some(_) | None => break, 250 | }, 251 | Some(c) if c.is_ascii() => buf.push(c), 252 | Some(c) => bail!("Invalid ado.net token `{}`", c), 253 | } 254 | } 255 | TokenKind::Escaped(buf) 256 | } 257 | '{' => { 258 | let mut buf = Vec::new(); 259 | // Read alphanumeric ASCII including whitespace until we find a closing curly. 260 | loop { 261 | match chars.next() { 262 | None => bail!("unclosed escape literal"), 263 | Some('}') => break, 264 | Some(c) if c.is_ascii() => buf.push(c), 265 | Some(c) => bail!("Invalid ado.net token `{}`", c), 266 | } 267 | } 268 | TokenKind::Escaped(buf) 269 | } 270 | ';' => TokenKind::Semi, 271 | '=' => TokenKind::Eq, 272 | '\n' => TokenKind::Newline, 273 | ' ' => TokenKind::Whitespace, 274 | char if char.is_ascii() => TokenKind::Atom(char), 275 | char => bail!("Invalid character found: {}", char), 276 | }; 277 | tokens.push(Token::new(kind, loc)); 278 | input = chars.as_str(); 279 | 280 | let consumed = old_input.len() - input.len(); 281 | loc.advance(&old_input[..consumed]); 282 | } 283 | tokens.reverse(); 284 | Ok(Self { tokens }) 285 | } 286 | 287 | /// Get the next token from the queue. 288 | #[must_use] 289 | pub(crate) fn next(&mut self) -> Token { 290 | self.tokens.pop().unwrap_or(Token { 291 | kind: TokenKind::Eof, 292 | loc: Location::default(), 293 | }) 294 | } 295 | 296 | /// Peek at the next token in the queue. 297 | #[must_use] 298 | pub(crate) fn peek(&mut self) -> Token { 299 | self.tokens.last().cloned().unwrap_or(Token { 300 | kind: TokenKind::Eof, 301 | loc: Location::default(), 302 | }) 303 | } 304 | } 305 | 306 | /// Look at the next char in the iterator. 307 | fn lookahead(iter: &std::str::Chars<'_>) -> Option { 308 | let s = iter.as_str(); 309 | s.chars().next() 310 | } 311 | 312 | /// Track the location of the Token inside the string. 313 | #[derive(Copy, Clone, Default, Debug)] 314 | pub(crate) struct Location { 315 | pub(crate) column: usize, 316 | } 317 | 318 | impl Location { 319 | fn advance(&mut self, text: &str) { 320 | self.column += text.chars().count(); 321 | } 322 | } 323 | 324 | #[cfg(test)] 325 | mod test { 326 | use super::AdoNetString; 327 | 328 | fn assert_kv(ado: &AdoNetString, key: &str, value: &str) { 329 | assert_eq!(ado.get(&key.to_lowercase()), Some(&value.to_owned())); 330 | } 331 | 332 | // Source: https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-string-syntax#windows-authentication-with-sqlclient 333 | // https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-string-syntax#windows-authentication-with-sqlclient 334 | #[test] 335 | fn windows_auth_with_sql_client() -> crate::Result<()> { 336 | let input = "Persist Security Info=False;Integrated Security=true;\nInitial Catalog=AdventureWorks;Server=MSSQL1"; 337 | let ado: AdoNetString = input.parse()?; 338 | assert_kv(&ado, "Persist Security Info", "False"); 339 | assert_kv(&ado, "Integrated Security", "true"); 340 | assert_kv(&ado, "Server", "MSSQL1"); 341 | assert_kv(&ado, "Initial Catalog", "AdventureWorks"); 342 | Ok(()) 343 | } 344 | 345 | // https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-string-syntax#sql-server-authentication-with-sqlclient 346 | #[test] 347 | fn sql_server_auth_with_sql_client() -> crate::Result<()> { 348 | let input = "Persist Security Info=False;User ID=*****;Password=*****;Initial Catalog=AdventureWorks;Server=MySqlServer"; 349 | let ado: AdoNetString = input.parse()?; 350 | assert_kv(&ado, "Persist Security Info", "False"); 351 | assert_kv(&ado, "User ID", "*****"); 352 | assert_kv(&ado, "Password", "*****"); 353 | assert_kv(&ado, "Initial Catalog", "AdventureWorks"); 354 | assert_kv(&ado, "Server", "MySqlServer"); 355 | Ok(()) 356 | } 357 | 358 | // https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-string-syntax#connect-to-a-named-instance-of-sql-server 359 | #[test] 360 | fn connect_to_named_sql_server_instance() -> crate::Result<()> { 361 | let input = r#"Data Source=MySqlServer\MSSQL1;"#; 362 | let ado: AdoNetString = input.parse()?; 363 | assert_kv(&ado, "Data Source", r#"MySqlServer\MSSQL1"#); 364 | Ok(()) 365 | } 366 | 367 | // https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-string-syntax#oledb-connection-string-syntax 368 | #[test] 369 | fn oledb_connection_string_syntax() -> crate::Result<()> { 370 | let input = r#"Provider=Microsoft.Jet.OLEDB.4.0; Data Source=d:\Northwind.mdb;User ID=Admin;Password=;"#; 371 | let ado: AdoNetString = input.parse()?; 372 | assert_kv(&ado, "Provider", r#"Microsoft.Jet.OLEDB.4.0"#); 373 | assert_kv(&ado, "Data Source", r#"d:\Northwind.mdb"#); 374 | assert_kv(&ado, "User ID", r#"Admin"#); 375 | assert_kv(&ado, "Password", r#""#); 376 | 377 | let input = r#"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=d:\Northwind.mdb;Jet OLEDB:System Database=d:\NorthwindSystem.mdw;User ID=*****;Password=*****;"#; 378 | let ado: AdoNetString = input.parse()?; 379 | assert_kv(&ado, "Provider", r#"Microsoft.Jet.OLEDB.4.0"#); 380 | assert_kv(&ado, "Data Source", r#"d:\Northwind.mdb"#); 381 | assert_kv( 382 | &ado, 383 | "Jet OLEDB:System Database", 384 | r#"d:\NorthwindSystem.mdw"#, 385 | ); 386 | assert_kv(&ado, "User ID", r#"*****"#); 387 | assert_kv(&ado, "Password", r#"*****"#); 388 | Ok(()) 389 | } 390 | 391 | // https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-string-syntax#using-datadirectory-to-connect-to-accessjet 392 | #[test] 393 | fn connect_to_access_jet() -> crate::Result<()> { 394 | let input = r#"Provider=Microsoft.Jet.OLEDB.4.0; 395 | Data Source=|DataDirectory|\Northwind.mdb; 396 | Jet OLEDB:System Database=|DataDirectory|\System.mdw;"#; 397 | let ado: AdoNetString = input.parse()?; 398 | assert_kv(&ado, "Data Source", r#"|DataDirectory|\Northwind.mdb"#); 399 | assert_kv(&ado, "Provider", r#"Microsoft.Jet.OLEDB.4.0"#); 400 | assert_kv( 401 | &ado, 402 | "Jet OLEDB:System Database", 403 | r#"|DataDirectory|\System.mdw"#, 404 | ); 405 | Ok(()) 406 | } 407 | 408 | // NOTE(yosh): we do not support Excel connection strings yet because the 409 | // double quote escaping is a small nightmare to parse. 410 | // // https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-string-syntax#connecting-to-excel 411 | // #[test] 412 | // fn connect_to_excel() -> crate::Result<()> { 413 | // let input = r#"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=D:\MyExcel.xls;Extended Properties=""Excel 8.0;HDR=Yes;IMEX=1"""#; 414 | // let ado: AdoNetString = input.parse()?; 415 | // assert_kv(&ado, "Provider", r#"Microsoft.Jet.OLEDB.4.0"#); 416 | // assert_kv(&ado, "Data Source", r#"D:\MyExcel.xls"#); 417 | // assert_kv( 418 | // &ado, 419 | // "Extended Properties", 420 | // r#"""Excel 8.0;HDR=Yes;IMEX=1"""#, 421 | // ); 422 | // Ok(()) 423 | // } 424 | 425 | // https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-string-syntax#data-shape-provider-connection-string-syntax 426 | #[test] 427 | fn data_shape_provider() -> crate::Result<()> { 428 | let input = r#"Provider=MSDataShape;Data Provider=SQLOLEDB;Data Source=(local);Initial Catalog=pubs;Integrated Security=SSPI;"#; 429 | let ado: AdoNetString = input.parse()?; 430 | assert_kv(&ado, "Provider", r#"MSDataShape"#); 431 | assert_kv(&ado, "Data Provider", r#"SQLOLEDB"#); 432 | assert_kv(&ado, "Data Source", r#"(local)"#); 433 | assert_kv(&ado, "Initial Catalog", r#"pubs"#); 434 | assert_kv(&ado, "Integrated Security", r#"SSPI"#); 435 | Ok(()) 436 | } 437 | 438 | // NOTE(yosh): we do not support ODBC connection strings because the first part of the 439 | // https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-string-syntax#odbc-connection-strings 440 | #[test] 441 | fn odbc_connection_strings() -> crate::Result<()> { 442 | let input = r#"Driver={Microsoft Text Driver (*.txt; *.csv)};DBQ=d:\bin"#; 443 | let ado: AdoNetString = input.parse()?; 444 | assert_kv(&ado, "Driver", r#"Microsoft Text Driver (*.txt; *.csv)"#); 445 | assert_kv(&ado, "DBQ", r#"d:\bin"#); 446 | Ok(()) 447 | } 448 | 449 | // https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-string-syntax#oracle-connection-strings 450 | #[test] 451 | fn oracle_connection_strings() -> crate::Result<()> { 452 | let input = "Data Source=Oracle9i;User ID=*****;Password=*****;"; 453 | let ado: AdoNetString = input.parse()?; 454 | assert_kv(&ado, "Data Source", "Oracle9i"); 455 | assert_kv(&ado, "User ID", "*****"); 456 | assert_kv(&ado, "Password", "*****"); 457 | Ok(()) 458 | } 459 | 460 | #[test] 461 | fn display_with_escaping() -> crate::Result<()> { 462 | let input = "key=val{;}ue"; 463 | let conn: AdoNetString = input.parse()?; 464 | 465 | assert_eq!(format!("{}", conn), input); 466 | 467 | Ok(()) 468 | } 469 | } 470 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::error; 2 | use std::fmt::{self, Display}; 3 | 4 | /// A connection string error. 5 | #[derive(Debug)] 6 | pub struct Error { 7 | msg: String, 8 | } 9 | 10 | /// Create a new Error. 11 | impl Error { 12 | /// Create a new instance of `Error`. 13 | pub fn new(msg: &str) -> Self { 14 | Self { 15 | msg: msg.to_owned(), 16 | } 17 | } 18 | } 19 | 20 | impl From for Error { 21 | fn from(err: std::num::ParseIntError) -> Self { 22 | Self { 23 | msg: format!("{}", err), 24 | } 25 | } 26 | } 27 | 28 | impl Display for Error { 29 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 30 | write!(f, "Conversion error: {}", self.msg) 31 | } 32 | } 33 | 34 | impl error::Error for Error {} 35 | -------------------------------------------------------------------------------- /src/jdbc.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | use std::{collections::HashMap, fmt::Display}; 3 | 4 | use crate::{bail, ensure}; 5 | 6 | /// JDBC connection string parser for SqlServer 7 | /// 8 | /// [Read more](https://docs.microsoft.com/en-us/sql/connect/jdbc/building-the-connection-url?view=sql-server-ver15) 9 | /// 10 | /// # Format 11 | /// 12 | /// ```txt 13 | /// jdbc:sqlserver://[serverName[\instanceName][:portNumber]][;property=value[;property=value]] 14 | /// ``` 15 | #[derive(Debug, PartialEq, Eq, Clone)] 16 | pub struct JdbcString { 17 | sub_protocol: String, 18 | server_name: Option, 19 | instance_name: Option, 20 | port: Option, 21 | properties: HashMap, 22 | } 23 | 24 | impl JdbcString { 25 | /// Access the connection sub-protocol 26 | pub fn sub_protocol(&self) -> &str { 27 | &self.sub_protocol 28 | } 29 | 30 | /// Access the connection server name 31 | pub fn server_name(&self) -> Option<&str> { 32 | self.server_name.as_deref() 33 | } 34 | 35 | /// Get a reference to the connection's instance name. 36 | pub fn instance_name(&self) -> Option<&str> { 37 | self.instance_name.as_deref() 38 | } 39 | 40 | /// Access the connection's port 41 | pub fn port(&self) -> Option { 42 | self.port 43 | } 44 | 45 | /// Access the connection's key-value pairs 46 | pub fn properties(&self) -> &HashMap { 47 | &self.properties 48 | } 49 | 50 | /// Mutably access the connection's key-value pairs 51 | pub fn properties_mut(&mut self) -> &mut HashMap { 52 | &mut self.properties 53 | } 54 | 55 | /// Get an iterator over all keys from the connection's key-value pairs 56 | pub fn keys(&self) -> impl ExactSizeIterator + '_ { 57 | self.properties.keys().map(AsRef::as_ref) 58 | } 59 | } 60 | 61 | impl Display for JdbcString { 62 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 63 | /// Escape all non-alphanumeric characters in a string.. 64 | fn escape(s: &str) -> String { 65 | let mut output = String::with_capacity(s.len()); 66 | let mut escaping = false; 67 | for b in s.chars() { 68 | if matches!(b, ':' | '=' | '\\' | '/' | ';' | '{' | '}' | '[' | ']') { 69 | if !escaping { 70 | escaping = true; 71 | output.push('{'); 72 | } 73 | output.push(b); 74 | } else { 75 | if escaping { 76 | escaping = false; 77 | output.push('}'); 78 | } 79 | output.push(b); 80 | } 81 | } 82 | if escaping { 83 | output.push('}'); 84 | } 85 | output 86 | } 87 | 88 | write!(f, "{}://", self.sub_protocol)?; 89 | if let Some(server_name) = &self.server_name { 90 | write!(f, "{}", escape(server_name))?; 91 | } 92 | if let Some(instance_name) = &self.instance_name { 93 | write!(f, r#"\{}"#, escape(instance_name))?; 94 | } 95 | if let Some(port) = self.port { 96 | write!(f, ":{}", port)?; 97 | } 98 | 99 | for (k, v) in self.properties().iter() { 100 | write!(f, ";{}={}", escape(k.trim()), escape(v.trim()))?; 101 | } 102 | Ok(()) 103 | } 104 | } 105 | 106 | // NOTE(yosh): Unfortunately we can't parse using `split(';')` because JDBC 107 | // strings support escaping. This means that `{;}` is valid and we need to write 108 | // an actual LR parser. 109 | impl FromStr for JdbcString { 110 | type Err = crate::Error; 111 | 112 | fn from_str(input: &str) -> Result { 113 | let mut lexer = Lexer::tokenize(input)?; 114 | 115 | // ``` 116 | // jdbc:sqlserver://[serverName[\instanceName][:portNumber]][;property=value[;property=value]] 117 | // ^^^^^^^^^^^^^^^^^ 118 | // ``` 119 | let err = "Invalid JDBC sub-protocol"; 120 | cmp_str(&mut lexer, "jdbc", err)?; 121 | ensure!(lexer.next().kind() == &TokenKind::Colon, err); 122 | let sub_protocol = format!("jdbc:{}", read_ident(&mut lexer, err)?); 123 | 124 | ensure!(lexer.next().kind() == &TokenKind::Colon, err); 125 | ensure!(lexer.next().kind() == &TokenKind::FSlash, err); 126 | ensure!(lexer.next().kind() == &TokenKind::FSlash, err); 127 | 128 | // ``` 129 | // jdbc:sqlserver://[serverName[\instanceName][:portNumber]][;property=value[;property=value]] 130 | // ^^^^^^^^^^^ 131 | // ``` 132 | // NOTE: this can also be an IPv6 address. 133 | let mut server_name = None; 134 | match lexer.peek().kind() { 135 | TokenKind::OpenBracket => { 136 | let err_msg = "Invalid server name: invalid IPv6 address"; 137 | server_name = Some(parse_ipv6(&mut lexer, err_msg)?); 138 | } 139 | TokenKind::Atom(_) | TokenKind::Escaped(_) => { 140 | server_name = Some(read_ident(&mut lexer, "Invalid server name")?); 141 | } 142 | _ => {} 143 | } 144 | 145 | // ``` 146 | // jdbc:sqlserver://[serverName[\instanceName][:portNumber]][;property=value[;property=value]] 147 | // ^^^^^^^^^^^^^^^ 148 | // ``` 149 | let mut instance_name = None; 150 | if matches!(lexer.peek().kind(), TokenKind::BSlash) { 151 | let _ = lexer.next(); 152 | instance_name = Some(read_ident(&mut lexer, "Invalid instance name")?); 153 | } 154 | 155 | // ``` 156 | // jdbc:sqlserver://[serverName[\instanceName][:portNumber]][;property=value[;property=value]] 157 | // ^^^^^^^^^^^^^ 158 | // ``` 159 | let mut port = None; 160 | if matches!(lexer.peek().kind(), TokenKind::Colon) { 161 | let _ = lexer.next(); 162 | let err = "Invalid port"; 163 | let s = read_ident(&mut lexer, err)?; 164 | port = Some(s.parse()?); 165 | } 166 | 167 | // ``` 168 | // jdbc:sqlserver://[serverName[\instanceName][:portNumber]][;property=value[;property=value]] 169 | // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 170 | // ``` 171 | // NOTE: we're choosing to only keep the last value per key rather than support multiple inserts per key. 172 | let mut properties = HashMap::new(); 173 | while let TokenKind::Semi = lexer.peek().kind() { 174 | let _ = lexer.next(); 175 | 176 | // Handle trailing semis. 177 | if let TokenKind::Eof = lexer.peek().kind() { 178 | let _ = lexer.next(); 179 | break; 180 | } 181 | 182 | let err = "Invalid property key"; 183 | let key = read_ident(&mut lexer, err)?.to_lowercase(); 184 | 185 | let err = "Property pairs must be joined by a `=`"; 186 | ensure!(lexer.next().kind() == &TokenKind::Eq, err); 187 | 188 | let err = "Invalid property value"; 189 | let value = read_ident(&mut lexer, err)?; 190 | 191 | properties.insert(key, value); 192 | } 193 | 194 | let token = lexer.next(); 195 | ensure!(token.kind() == &TokenKind::Eof, "Invalid JDBC token"); 196 | 197 | Ok(Self { 198 | sub_protocol, 199 | server_name, 200 | instance_name, 201 | port, 202 | properties, 203 | }) 204 | } 205 | } 206 | 207 | /// Validate a sequence of `TokenKind::Atom` matches the content of a string. 208 | fn cmp_str(lexer: &mut Lexer, s: &str, err_msg: &'static str) -> crate::Result<()> { 209 | for char in s.chars() { 210 | if let Token { 211 | kind: TokenKind::Atom(tchar), 212 | .. 213 | } = lexer.next() 214 | { 215 | ensure!(char == tchar, err_msg); 216 | } else { 217 | bail!(err_msg); 218 | } 219 | } 220 | Ok(()) 221 | } 222 | 223 | /// Read sequences of `TokenKind::Atom` and `TokenKind::Escaped` into a String. 224 | fn read_ident(lexer: &mut Lexer, err_msg: &'static str) -> crate::Result { 225 | let mut output = String::new(); 226 | loop { 227 | let token = lexer.next(); 228 | match token.kind() { 229 | TokenKind::Escaped(seq) => output.extend(seq), 230 | TokenKind::Atom(c) => output.push(*c), 231 | _ => { 232 | // push the token back in the lexer 233 | lexer.push(token); 234 | break; 235 | } 236 | } 237 | } 238 | ensure!(!output.is_empty(), err_msg); 239 | Ok(output) 240 | } 241 | 242 | /// Read a URL encoded IPv6 sequence into a string. 243 | /// 244 | /// Example: `[2001:db8:85a3:8d3:1319:8a2e:370:7348]` 245 | /// 246 | /// See also: https://en.wikipedia.org/wiki/IPv6_address#Literal_IPv6_addresses_in_network_resource_identifiers 247 | fn parse_ipv6(lexer: &mut Lexer, err_msg: &'static str) -> crate::Result { 248 | let _ = lexer.next(); 249 | let mut output = String::from('['); 250 | 251 | loop { 252 | match lexer.next().kind() { 253 | TokenKind::Colon => output.push(':'), 254 | TokenKind::Atom(c) if c.is_ascii_alphanumeric() => output.push(*c), 255 | TokenKind::CloseBracket => { 256 | output.push(']'); 257 | break; 258 | } 259 | _ => bail!(err_msg), 260 | } 261 | } 262 | 263 | ensure!(!output.is_empty(), err_msg); 264 | Ok(output) 265 | } 266 | 267 | #[derive(Debug)] 268 | struct Lexer { 269 | tokens: Vec, 270 | } 271 | 272 | impl Lexer { 273 | /// Parse a string into a list of tokens. 274 | pub(crate) fn tokenize(mut input: &str) -> crate::Result { 275 | let mut tokens = vec![]; 276 | let mut loc = Location::default(); 277 | while !input.is_empty() { 278 | let old_input = input; 279 | let mut chars = input.chars(); 280 | let kind = match chars.next().unwrap() { 281 | // c if c.is_ascii_whitespace() => continue, 282 | ':' => TokenKind::Colon, 283 | '=' => TokenKind::Eq, 284 | '\\' => TokenKind::BSlash, 285 | '/' => TokenKind::FSlash, 286 | ';' => TokenKind::Semi, 287 | '[' => TokenKind::OpenBracket, 288 | ']' => TokenKind::CloseBracket, 289 | '{' => { 290 | let mut buf = Vec::new(); 291 | // Read alphanumeric ASCII including whitespace until we find a closing curly. 292 | loop { 293 | match chars.next() { 294 | None => bail!("unclosed escape literal"), 295 | Some('}') => break, 296 | Some(c) if c.is_ascii() => buf.push(c), 297 | Some(c) => bail!("Invalid JDBC token `{}`", c), 298 | } 299 | } 300 | TokenKind::Escaped(buf) 301 | } 302 | c if c.is_ascii() => TokenKind::Atom(c), 303 | c => bail!("Invalid JDBC token `{}`", c), 304 | }; 305 | tokens.push(Token { kind, loc }); 306 | input = chars.as_str(); 307 | 308 | let consumed = old_input.len() - input.len(); 309 | loc.advance(&old_input[..consumed]); 310 | } 311 | tokens.reverse(); 312 | Ok(Self { tokens }) 313 | } 314 | 315 | /// Get the next token from the queue. 316 | #[must_use] 317 | pub(crate) fn next(&mut self) -> Token { 318 | self.tokens.pop().unwrap_or(Token { 319 | kind: TokenKind::Eof, 320 | loc: Location::default(), 321 | }) 322 | } 323 | 324 | /// Push a token back onto the queue. 325 | pub(crate) fn push(&mut self, token: Token) { 326 | self.tokens.push(token); 327 | } 328 | 329 | /// Peek at the next token in the queue. 330 | #[must_use] 331 | pub(crate) fn peek(&mut self) -> Token { 332 | self.tokens.last().cloned().unwrap_or(Token { 333 | kind: TokenKind::Eof, 334 | loc: Location::default(), 335 | }) 336 | } 337 | } 338 | 339 | /// Track the location of the Token inside the string. 340 | #[derive(Copy, Clone, Default, Debug)] 341 | pub(crate) struct Location { 342 | pub(crate) column: usize, 343 | } 344 | 345 | impl Location { 346 | fn advance(&mut self, text: &str) { 347 | self.column += text.chars().count(); 348 | } 349 | } 350 | 351 | /// A pair of `Location` and `TokenKind`. 352 | #[derive(Debug, Clone)] 353 | struct Token { 354 | #[allow(dead_code)] // for future use... 355 | loc: Location, 356 | kind: TokenKind, 357 | } 358 | 359 | impl Token { 360 | /// What kind of token is this? 361 | pub(crate) fn kind(&self) -> &TokenKind { 362 | &self.kind 363 | } 364 | } 365 | 366 | /// The kind of token we're encoding. 367 | #[derive(Debug, Clone, Eq, PartialEq)] 368 | enum TokenKind { 369 | OpenBracket, 370 | CloseBracket, 371 | Colon, 372 | Eq, 373 | BSlash, 374 | FSlash, 375 | Semi, 376 | /// An ident that falls inside a `{}` pair. 377 | Escaped(Vec), 378 | /// An identifier in the connection string. 379 | Atom(char), 380 | Eof, 381 | } 382 | 383 | #[cfg(test)] 384 | mod test { 385 | use super::JdbcString; 386 | 387 | #[test] 388 | fn parse_sub_protocol() -> crate::Result<()> { 389 | let conn: JdbcString = "jdbc:sqlserver://".parse()?; 390 | assert_eq!(conn.sub_protocol(), "jdbc:sqlserver"); 391 | Ok(()) 392 | } 393 | 394 | #[test] 395 | fn keys() -> crate::Result<()> { 396 | let input = r#"jdbc:sqlserver://server:1433;database=prisma-demo;user=SA;password=Pr1sm4_Pr1sm4;trustServerCertificate=true;encrypt=true"#; 397 | let conn: JdbcString = input.parse()?; 398 | let keys = conn.keys().collect::>(); 399 | assert_eq!(keys.len(), 5); 400 | assert!(keys.contains(&"database")); 401 | assert!(keys.contains(&"user")); 402 | assert!(keys.contains(&"password")); 403 | assert!(keys.contains(&"trustservercertificate")); 404 | assert!(keys.contains(&"encrypt")); 405 | Ok(()) 406 | } 407 | 408 | #[test] 409 | fn parse_server_name() -> crate::Result<()> { 410 | let conn: JdbcString = r#"jdbc:sqlserver://server"#.parse()?; 411 | assert_eq!(conn.sub_protocol(), "jdbc:sqlserver"); 412 | assert_eq!(conn.server_name(), Some("server")); 413 | Ok(()) 414 | } 415 | 416 | #[test] 417 | fn parse_instance_name() -> crate::Result<()> { 418 | let conn: JdbcString = r#"jdbc:sqlserver://server\instance"#.parse()?; 419 | assert_eq!(conn.sub_protocol(), "jdbc:sqlserver"); 420 | assert_eq!(conn.server_name(), Some("server")); 421 | assert_eq!(conn.instance_name(), Some("instance")); 422 | Ok(()) 423 | } 424 | 425 | #[test] 426 | fn parse_ipv6_url() -> crate::Result<()> { 427 | let input = r#"jdbc:sqlserver://[::1]:1433;database=prisma-demo;user=SA;password=Pr1sm4_Pr1sm4;trustServerCertificate=true;encrypt=true"#; 428 | let conn: JdbcString = input.parse()?; 429 | assert_eq!(conn.server_name(), Some("[::1]")); 430 | assert_eq!(conn.port(), Some(1433)); 431 | 432 | let input = r#"jdbc:sqlserver://[:1433;"#; 433 | assert!(input.parse::().is_err()); 434 | 435 | let input = r#"jdbc:sqlserver://[0f0f0:f00f==:09:12]:1433;"#; 436 | assert!(input.parse::().is_err()); 437 | 438 | let input = r#"jdbc:sqlserver://]:1433;"#; 439 | assert!(input.parse::().is_err()); 440 | Ok(()) 441 | } 442 | 443 | #[test] 444 | fn parse_port() -> crate::Result<()> { 445 | let conn: JdbcString = r#"jdbc:sqlserver://server\instance:80"#.parse()?; 446 | assert_eq!(conn.sub_protocol(), "jdbc:sqlserver"); 447 | assert_eq!(conn.server_name(), Some("server")); 448 | assert_eq!(conn.instance_name(), Some("instance")); 449 | assert_eq!(conn.port(), Some(80)); 450 | Ok(()) 451 | } 452 | 453 | #[test] 454 | fn parse_properties() -> crate::Result<()> { 455 | let conn: JdbcString = 456 | r#"jdbc:sqlserver://server\instance:80;key=value;foo=bar"#.parse()?; 457 | assert_eq!(conn.sub_protocol(), "jdbc:sqlserver"); 458 | assert_eq!(conn.server_name(), Some("server")); 459 | assert_eq!(conn.instance_name(), Some("instance")); 460 | assert_eq!(conn.port(), Some(80)); 461 | 462 | let kv = conn.properties(); 463 | assert_eq!(kv.get("foo"), Some(&"bar".to_string())); 464 | assert_eq!(kv.get("key"), Some(&"value".to_string())); 465 | Ok(()) 466 | } 467 | 468 | #[test] 469 | fn escaped_properties() -> crate::Result<()> { 470 | let conn: JdbcString = 471 | r#"jdbc:sqlserver://se{r}ver{;}\instance:80;key={va[]}lue"#.parse()?; 472 | assert_eq!(conn.sub_protocol(), "jdbc:sqlserver"); 473 | assert_eq!(conn.server_name(), Some("server;")); 474 | assert_eq!(conn.instance_name(), Some("instance")); 475 | assert_eq!(conn.port(), Some(80)); 476 | 477 | let kv = conn.properties(); 478 | assert_eq!(kv.get("key"), Some(&"va[]lue".to_string())); 479 | Ok(()) 480 | } 481 | 482 | #[test] 483 | fn sub_protocol_error() -> crate::Result<()> { 484 | let err = r#"jdbq:sqlserver://"#.parse::().unwrap_err().to_string(); 485 | assert_eq!(err, "Conversion error: Invalid JDBC sub-protocol"); 486 | Ok(()) 487 | } 488 | 489 | #[test] 490 | fn whitespace() -> crate::Result<()> { 491 | let conn: JdbcString = 492 | r#"jdbc:sqlserver://server\instance:80;key=value;foo=bar;user id=musti naukio"# 493 | .parse()?; 494 | assert_eq!(conn.sub_protocol(), "jdbc:sqlserver"); 495 | assert_eq!(conn.server_name(), Some(r#"server"#)); 496 | assert_eq!(conn.instance_name(), Some("instance")); 497 | assert_eq!(conn.port(), Some(80)); 498 | 499 | let kv = conn.properties(); 500 | assert_eq!(kv.get("user id"), Some(&"musti naukio".to_string())); 501 | Ok(()) 502 | } 503 | 504 | // Test for dashes and dots in the name, and parse names other than oracle 505 | #[test] 506 | fn regression_2020_10_06() -> crate::Result<()> { 507 | let input = "jdbc:sqlserver://my-server.com:5433;foo=bar"; 508 | let _conn: JdbcString = input.parse()?; 509 | 510 | let input = "jdbc:oracle://foo.bar:1234"; 511 | let _conn: JdbcString = input.parse()?; 512 | 513 | Ok(()) 514 | } 515 | 516 | /// While strictly disallowed, we should not fail if we detect a trailing semi. 517 | #[test] 518 | fn regression_2020_10_07_handle_trailing_semis() -> crate::Result<()> { 519 | let input = "jdbc:sqlserver://my-server.com:5433;foo=bar;"; 520 | let _conn: JdbcString = input.parse()?; 521 | 522 | let input = "jdbc:sqlserver://my-server.com:4200;User ID=musti;Password={abc;}}45}"; 523 | let conn: JdbcString = input.parse()?; 524 | let props = conn.properties(); 525 | assert_eq!(props.get("user id"), Some(&"musti".to_owned())); 526 | assert_eq!(props.get("password"), Some(&"abc;}45}".to_owned())); 527 | Ok(()) 528 | } 529 | 530 | #[test] 531 | fn display_with_escaping() -> crate::Result<()> { 532 | let input = r#"jdbc:sqlserver://server{;}\instance:80;key=va{[]}lue"#; 533 | let conn: JdbcString = input.parse()?; 534 | 535 | assert_eq!(format!("{}", conn), input); 536 | Ok(()) 537 | } 538 | 539 | // Output was being over-escaped and not split with semis, causing all sorts of uri failures. 540 | #[test] 541 | fn regression_2020_10_27_dont_escape_underscores_whitespace() -> crate::Result<()> { 542 | let input = r#"jdbc:sqlserver://test-db-mssql-2017:1433;user=SA;encrypt=DANGER_PLAINTEXT;isolationlevel=READ UNCOMMITTED;schema=NonEmbeddedUpsertDesignSpec;trustservercertificate=true;password="#; 543 | let conn: JdbcString = input.parse()?; 544 | 545 | let output = format!("{}", conn); 546 | let mut output: Vec = output.split(';').map(|s| s.to_owned()).collect(); 547 | output.pop(); 548 | output.sort(); 549 | 550 | let input = format!("{}", conn); 551 | let mut input: Vec = input.split(';').map(|s| s.to_owned()).collect(); 552 | input.pop(); 553 | input.sort(); 554 | 555 | assert_eq!(output, input); 556 | Ok(()) 557 | } 558 | } 559 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Connection string parsing in Rust 2 | //! 3 | //! # Examples 4 | //! 5 | //! JDBC 6 | //! ``` 7 | //! use connection_string::JdbcString; 8 | //! 9 | //! let conn: JdbcString = r#"jdbc:sqlserver://server\instance:80;key=value;foo=bar"#.parse().unwrap(); 10 | //! assert_eq!(conn.sub_protocol(), "jdbc:sqlserver"); 11 | //! ``` 12 | //! 13 | //! Ado.net 14 | //! ``` 15 | //! use connection_string::AdoNetString; 16 | //! 17 | //! let input = "Persist Security Info=False;Integrated Security=true;\nInitial Catalog=AdventureWorks;Server=MSSQL1"; 18 | //! let _: AdoNetString = input.parse().unwrap(); 19 | //! ``` 20 | 21 | #![forbid(unsafe_code, rust_2018_idioms)] 22 | #![deny(warnings, missing_debug_implementations, nonstandard_style)] 23 | #![warn(missing_docs, future_incompatible, unreachable_pub)] 24 | 25 | mod ado; 26 | mod error; 27 | mod jdbc; 28 | 29 | #[macro_use] 30 | mod utils; 31 | 32 | pub use ado::AdoNetString; 33 | pub use jdbc::JdbcString; 34 | 35 | pub use error::Error; 36 | type Result = std::result::Result; 37 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | // Return early with an error if a condition is not satisfied. 2 | #[doc(hidden)] 3 | #[macro_export] 4 | macro_rules! ensure { 5 | ($cond:expr, $msg:literal) => { 6 | if !$cond { 7 | return Err($crate::Error::new($msg.into())); 8 | }; 9 | }; 10 | 11 | ($cond:expr, $msg:expr) => { 12 | if !$cond { 13 | return Err($crate::Error::new($msg.into())); 14 | }; 15 | }; 16 | } 17 | 18 | // Return early with an error. 19 | #[doc(hidden)] 20 | #[macro_export] 21 | macro_rules! bail { 22 | ($msg:literal) => { 23 | return Err($crate::Error::new($msg.into())) 24 | }; 25 | 26 | ($msg:expr) => { 27 | return Err($crate::Error::new($msg.into())) 28 | }; 29 | 30 | ($fmt:expr, $($arg:tt)*) => { 31 | return Err($crate::Error::new(&*format!($fmt, $($arg)*))) 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /tests/test.rs: -------------------------------------------------------------------------------- 1 | 2 | --------------------------------------------------------------------------------