├── .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 |
32 |
33 |
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 |
--------------------------------------------------------------------------------