├── .VERSION_PREFIX ├── .circleci └── config.yml ├── .dir-locals.el ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bb.edn ├── bin ├── kaocha └── proj ├── deps.edn ├── generate_docs ├── package.json ├── pom.xml ├── repl_sessions ├── encode.clj ├── merge_join.clj ├── poke.clj ├── round_trip_query_map.clj ├── uri_url_file_path.clj └── walkthrough.clj ├── src └── lambdaisland │ ├── uri.cljc │ └── uri │ ├── normalize.cljc │ ├── platform.clj │ └── platform.cljs ├── test └── lambdaisland │ ├── test_runner.cljs │ ├── uri │ └── normalize_test.cljc │ └── uri_test.cljc └── tests.edn /.VERSION_PREFIX: -------------------------------------------------------------------------------- 1 | 1.19 -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | kaocha: lambdaisland/kaocha@0.0.1 5 | clojure: lambdaisland/clojure@0.0.8 6 | 7 | commands: 8 | checkout_and_run: 9 | parameters: 10 | clojure_version: 11 | type: string 12 | steps: 13 | - checkout 14 | - clojure/with_cache: 15 | cache_version: << parameters.clojure_version >> 16 | steps: 17 | - run: clojure -e '(println (System/getProperty "java.runtime.name") (System/getProperty "java.runtime.version") "\nClojure" (clojure-version))' 18 | - run: npm install ws 19 | - kaocha/execute: 20 | args: "clj --reporter documentation --plugin cloverage --codecov" 21 | clojure_version: << parameters.clojure_version >> 22 | - kaocha/execute: 23 | args: "cljs --reporter documentation" 24 | clojure_version: << parameters.clojure_version >> 25 | - kaocha/upload_codecov: 26 | flags: clj 27 | 28 | jobs: 29 | java-17-clojure-1_10: 30 | executor: clojure/openjdk17 31 | steps: [{checkout_and_run: {clojure_version: "1.10.2"}}] 32 | 33 | java-11-clojure-1_10: 34 | executor: clojure/openjdk11 35 | steps: [{checkout_and_run: {clojure_version: "1.10.2"}}] 36 | 37 | java-9-clojure-1_10: 38 | executor: clojure/openjdk9 39 | steps: [{checkout_and_run: {clojure_version: "1.10.2"}}] 40 | 41 | java-8-clojure-1_10: 42 | executor: clojure/openjdk8 43 | steps: [{checkout_and_run: {clojure_version: "1.10.2"}}] 44 | test: 45 | parameters: 46 | os: 47 | type: executor 48 | clojure_version: 49 | type: string 50 | executor: << parameters.os >> 51 | steps: 52 | - checkout_and_run: 53 | clojure_version: << parameters.clojure_version >> 54 | 55 | babashka-tests: 56 | executor: clojure/openjdk8 57 | steps: 58 | - checkout 59 | - run: 60 | command: | 61 | curl -sLO https://raw.githubusercontent.com/babashka/babashka/master/install 62 | chmod +x install 63 | ./install --version 1.0.168 64 | bb test:bb 65 | 66 | workflows: 67 | kaocha_test: 68 | jobs: 69 | - babashka-tests 70 | - test: 71 | matrix: 72 | parameters: 73 | os: [clojure/openjdk19, clojure/openjdk17, clojure/openjdk11, clojure/openjdk8] 74 | clojure_version: ["1.10.3", "1.11.1"] 75 | -------------------------------------------------------------------------------- /.dir-locals.el: -------------------------------------------------------------------------------- 1 | ((nil . ((cider-clojure-cli-global-options . "-A:dev:test")))) 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml.asc 5 | *.jar 6 | *.class 7 | /.lein-* 8 | /.nrepl-port 9 | .hgignore 10 | .hg/ 11 | gh-pages 12 | .cljs_rhino_repl 13 | .cljs_node_repl 14 | out 15 | node_modules 16 | .cpcache 17 | package-lock.json 18 | .store 19 | .idea/ 20 | .clj-kondo/ 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | install: 3 | - curl https://raw.githubusercontent.com/technomancy/leiningen/stable/bin/lein > my-lein 4 | - chmod +x my-lein 5 | - ./my-lein deps 6 | - ./my-lein --version 7 | script: ./my-lein test-all 8 | jdk: 9 | - oraclejdk8 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Unreleased 2 | 3 | ## Added 4 | 5 | ## Fixed 6 | 7 | ## Changed 8 | 9 | # 1.19.155 (2024-01-24 / a0d4895) 10 | 11 | ## Added 12 | 13 | - Support ipv6 literals 14 | 15 | # 1.18.150 (2024-01-24 / 1e4f0e2) 16 | 17 | ## Changed 18 | 19 | - `query-map` / `query-string->map`: when called with an `:into` collection, and 20 | no query params are present, then return the `:into` collection, rather than 21 | `nil`. If no explicit `:into` collection is provided, then retain the existing 22 | behavior of returning `nil` on blank input. 23 | 24 | # 1.17.141 (2024-01-23 / 96249d9) 25 | 26 | ## Added 27 | 28 | - Added functions for dealing with query strings as positional collections: 29 | `query-string->seq`, `seq->query-string`. 30 | 31 | ## Changed 32 | 33 | - `query-map`/`query-string->map` : return `:into` value on blank input 34 | 35 | # 1.16.134 (2023-10-10 / c0f16d8) 36 | 37 | ## Fixed 38 | 39 | - Do not truncate value of a query parameter pair when contains nested `=` characters 40 | 41 | # 1.15.125 (2023-03-30 / 5550226) 42 | 43 | ## Added 44 | 45 | - Adds `:into` option to define custom `clojure.lang.IPersistentMap` target data structure for `lambdaisland.uri/query-string->map` 46 | 47 | # 1.14.120 (2023-03-27 / a1da1b7) 48 | 49 | ## Fixed 50 | 51 | - Treat a backslash in the authority section as a delimiter which starts the 52 | path section (CVE-2023-28628, with thanks to @luigigubello for the report) 53 | 54 | # 1.13.95 (2022-01-28 / a9cbeff) 55 | 56 | ## Fixed 57 | 58 | - Fix a stack overflow in `normalize/char-seq` for really large query parameter 59 | values 60 | 61 | # 1.12.89 (2021-11-29 / 2118a75) 62 | 63 | ## Changed 64 | 65 | - Support `toString` on Babashka (requires recent `bb`) 66 | 67 | # 1.11.86 (2021-10-28 / 22c27af) 68 | 69 | ## Fixed 70 | 71 | - Fixed an issue in `lambdaisland.uri.normalize/normalize-query` which did 72 | not take into account utf-16 encoding. 73 | 74 | # 1.10.79 (2021-10-12 / d90c6a8) 75 | 76 | ## Changed 77 | 78 | - `lambdaisland.uri.normalize/normalize` now also normalizes the fragment. 79 | 80 | # 1.4.74 (2021-09-06 / e07f9fd) 81 | 82 | ## Added 83 | 84 | - `uri-str` as an explicit `lambdaisland.uri.URI` to string conversion 85 | 86 | ## Fixed 87 | 88 | - Fixed compatibility with Babashka/SCI. Note that on babashka we can't 89 | implement IFn or toString, so converting a `URI` back to a string needs to be 90 | done explicitly with `uri-str`, and it is not possible to use a URI as a 91 | function. (`(:path uri)` is ok, `(uri :path)` is not). 92 | 93 | # 1.4.70 (2021-05-31 / 76999dc) 94 | 95 | ## Added 96 | 97 | - Add `uri?` predicate. 98 | 99 | # 1.4.54 (2020-06-16 / 05a8a19) 100 | 101 | ## Fixed 102 | 103 | - Make query decoding handle `+` as space, so the conversion between maps and 104 | query strings correctly round trips. 105 | - Handle percent encoding of control characters (codepoints < 16) 106 | - make `lambdaisland.uri.platform/string->byte-seq` return unsigned bytes on 107 | both plaforms (clj/cljs) 108 | 109 | # 1.4.49 (2020-06-11 / ee48e58) 110 | 111 | ## Changed 112 | 113 | - Make `assoc-query` / `query-encode` encode spaces as "+" rather than "%20", 114 | which brings it in line to how most languages/libraries do it. 115 | 116 | # 1.3.45 (2020-05-01 / a04368b) 117 | 118 | ## Added 119 | 120 | - Added function for dealing with query strings as maps: `query-string->map`, 121 | `map->query-string`, `query-map`, `query-encode`, `assoc-query`, 122 | `assoc-query*`. 123 | 124 | ## Fixed 125 | 126 | - Fix query string normalization, for delimiter characters like `=` and `+` 127 | there is a semantic difference between the encoded and decoded form, when they 128 | are encoded in the input normalization should not decode them and vice versa 129 | 130 | # 1.2.1 (2020-02-23 / a992787) 131 | 132 | ## Changed 133 | 134 | - Remove dependencies on ClojureScript and data.json. 135 | 136 | # 1.2.0 (2020-02-17 / c0e1f1a) 137 | 138 | ## Added 139 | 140 | - `lambdaisland.uri.normalize/normalize`, for normalizing URI instances. 141 | 142 | ## Changed 143 | 144 | - Added type hints to avoid reflection (thanks @totakke!) 145 | 146 | # 1.1.0 (2017-04-25) 147 | 148 | ## Added 149 | 150 | - Predicate functions `absolute?` and `relative?` 151 | 152 | # 1.0.0 (2017-02-23) 153 | 154 | ## Added 155 | 156 | - Initial release, public vars: `uri`, `join`, `coerce`, `parse`, `edn-readers` -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lambdaisland/uri 2 | 3 | 4 | [![CircleCI](https://circleci.com/gh/lambdaisland/uri.svg?style=svg)](https://circleci.com/gh/lambdaisland/uri) [![cljdoc badge](https://cljdoc.org/badge/lambdaisland/uri)](https://cljdoc.org/d/lambdaisland/uri) [![Clojars Project](https://img.shields.io/clojars/v/lambdaisland/uri.svg)](https://clojars.org/lambdaisland/uri) 5 | [![bb compatible](https://raw.githubusercontent.com/babashka/babashka/master/logo/badge.svg)](https://babashka.org) 6 | 7 | 8 | A pure Clojure/ClojureScript URI library. 9 | 10 | Key features 11 | 12 | - 100% cross-platform `.cljc` 13 | - RFC compliant joining of URIs 14 | - relative URIs as first class citizens 15 | 16 | 17 | ## Lambda Island Open Source 18 | 19 | 20 | 21 |   22 | 23 | uri is part of a growing collection of quality Clojure libraries created and maintained 24 | by the fine folks at [Gaiwan](https://gaiwan.co). 25 | 26 | Pay it forward by [becoming a backer on our Open Collective](http://opencollective.com/lambda-island), 27 | so that we may continue to enjoy a thriving Clojure ecosystem. 28 | 29 | You can find an overview of our projects at [lambdaisland/open-source](https://github.com/lambdaisland/open-source). 30 | 31 |   32 | 33 |   34 | 35 | 36 | ## Rationale 37 | 38 | There are a number of Clojure libraries for working with URI/URLs (see 39 | [Similar projects](#similar-projects) below). They all rely to some degree on 40 | `java.net.URI` or `java.net.URL`. This lib provides a pure-Clojure/ClojureScript 41 | alternative. 42 | 43 | See the [announcement blog post](https://lambdaisland.com/blog/27-02-2017-announcing-lambdaisland-uri) 44 | 45 | ## Installation 46 | 47 | To install, add the following dependency to your project or build file: 48 | 49 | deps.edn: 50 | 51 | ``` clojure 52 | lambdaisland/uri {:mvn/version "1.19.155"} 53 | ``` 54 | 55 | project.clj 56 | 57 | ``` clojure 58 | [lambdaisland/uri "1.19.155"] 59 | ``` 60 | 61 | ## Usage 62 | 63 | ``` clojure 64 | (require '[lambdaisland.uri :refer [uri join]]) 65 | 66 | 67 | ;; uri :: String -> lambdaisland.uri.URI 68 | (uri "//example.com/foo/bar") 69 | ;;=> #lambdaisland/uri "//example.com/foo/bar" 70 | 71 | 72 | ;; A URI is a record, use assoc to update specific parts 73 | ;; Use `str` if you want the URI back as a string 74 | (str 75 | (assoc (uri "//example.com/foo/bar") 76 | :scheme "https" 77 | :user "arne" 78 | :password "supersecret" 79 | :host "lambdaisland.com" 80 | :port "3333" 81 | :path "/hello/world" 82 | :query "q=5" 83 | :fragment "section1")) 84 | ;;=> "https://arne:supersecret@lambdaisland.com:3333/hello/world?q=5#section1" 85 | 86 | 87 | ;; RFC compliant joining of relative URIs 88 | (join "//example.com/foo/bar" "./~arne/site/" "../foo.png") 89 | ;;=> #lambdaisland/uri "//example.com/foo/~arne/foo.png" 90 | 91 | 92 | ;; Arguments to `join` are coerced, you can pass strings, java.net.URI, or any x 93 | ;; for which `(str x)` returns a URI string. 94 | (join (java.net.URI. "http://example.com/foo/bar") (uri "./~arne/site/") "../foo.png") 95 | ;;=> #lambdaisland/uri "http://example.com/foo/~arne/foo.png" 96 | 97 | 98 | ;; URI implements IFn for keyword based lookup, so it's fully 99 | ;; interface-compatible with Clojure maps. 100 | (:path (uri "http://example.com/foo/bar")) 101 | 102 | ;; Provide custom ordering for query-map 103 | ;; clj -Sdeps '{:deps {org.flatland/ordered {:mvn/version "1.5.7"}}}' 104 | (require '[lambdaisland.uri :refer [query-map]] 105 | '[flatland.ordered.map :refer [ordered-map]]) 106 | (keys (query-map "http://example.com?a=1&b=2&c=3&d=4&e=5&f=6&g=7&h=8&i=9" 107 | {:into (ordered-map)})) 108 | => (:a :b :c :d :e :f :g :h :i) 109 | 110 | ;; Instances of URI are printed with a #lambdaisland/uri reader tag. To read 111 | ;; them back from EDN, use the provided readers. 112 | (require '[clojure.edn :as edn]) 113 | 114 | (edn/read-string 115 | {:readers lambdaisland.uri/edn-readers} 116 | "#lambdaisland/uri \"http://example.com/foo/~arne/foo.png\"") 117 | ``` 118 | 119 | [Full API docs are on Cljdoc](https://cljdoc.org/d/lambdaisland/uri) 120 | 121 | ## Babashka-specific caveats (also applies to SCI) 122 | 123 | Instances of URI implement the `toString` method, so calling `(str uri)` gives 124 | you the URI back as a string. They also implement the `IFn` interfaces so they 125 | are callable the way maps are. 126 | 127 | On babashka implementing interfaces or overriding Object methods is not 128 | supported. As an alternative to `clojure.core/str` you can use 129 | `lambdaisland.uri/uri-str`. As an alternative to using the URI as a function, use the keyword as a function, or use `clojure.core/get` 130 | 131 | ``` clojure 132 | ;; clojure / clojurescript 133 | (str uri) ;; "https://example.com" 134 | (uri :host) ;; "example.com" 135 | 136 | ;; bb 137 | (str uri) ;; "{:scheme "https", :domain "example.com", :path ...}" 138 | (uri :host) ;; nil 139 | 140 | (uri/uri-str uri) ;; "https://example.com" 141 | (:host uri) ;; "example.com" 142 | (get uri :host) ;; "example.com" 143 | ``` 144 | 145 | ## Similar projects 146 | 147 | * [exploding-fish](https://github.com/wtetzner/exploding-fish) 148 | I was not aware at the time of creating lambdaisland/uri of exploding fish. It 149 | is the most mature pure-Clojure URI lib out there. It does not provide 150 | ClojureScript support. 151 | * [cemerick/url](https://github.com/cemerick/url) 152 | Cross platform (cljx), Clojure version uses `java.net.URL`. 153 | * [michaelklishin/urly](https://github.com/michaelklishin/urly) 154 | Based on `java.net.URI`. 155 | * [codonnell/uri](https://github.com/codonnell/uri) 156 | Based on `java.net.URI`. 157 | 158 | ## Further reading 159 | 160 | [RFC3986 Uniform Resource Identifier (URI): Generic Syntax](https://www.ietf.org/rfc/rfc3986.txt) 161 | 162 | This library implements the algorithm specified in [Section 5.2](https://tools.ietf.org/html/rfc3986#section-5.2) of that RFC. 163 | 164 | It has been tested against [this list of test cases compiled by the W3C](https://www.w3.org/2004/04/uri-rel-test.html). 165 | 166 | 167 | ## Contributing 168 | 169 | Everyone has a right to submit patches to uri, and thus become a contributor. 170 | 171 | Contributors MUST 172 | 173 | - adhere to the [LambdaIsland Clojure Style Guide](https://nextjournal.com/lambdaisland/clojure-style-guide) 174 | - write patches that solve a problem. Start by stating the problem, then supply a minimal solution. `*` 175 | - agree to license their contributions as MPL 2.0. 176 | - not break the contract with downstream consumers. `**` 177 | - not break the tests. 178 | 179 | Contributors SHOULD 180 | 181 | - update the CHANGELOG and README. 182 | - add tests for new functionality. 183 | 184 | If you submit a pull request that adheres to these rules, then it will almost 185 | certainly be merged immediately. However some things may require more 186 | consideration. If you add new dependencies, or significantly increase the API 187 | surface, then we need to decide if these changes are in line with the project's 188 | goals. In this case you can start by [writing a pitch](https://nextjournal.com/lambdaisland/pitch-template), 189 | and collecting feedback on it. 190 | 191 | `*` This goes for features too, a feature needs to solve a problem. State the problem it solves, then supply a minimal solution. 192 | 193 | `**` As long as this project has not seen a public release (i.e. is not on Clojars) 194 | we may still consider making breaking changes, if there is consensus that the 195 | changes are justified. 196 | 197 | 198 | 199 | ## License 200 | 201 | Copyright © 2017-2021 Arne Brasseur and Contributors 202 | 203 | Licensed under the term of the Mozilla Public License 2.0, see LICENSE. 204 | 205 | -------------------------------------------------------------------------------- /bb.edn: -------------------------------------------------------------------------------- 1 | {:deps 2 | {lambdaisland/open-source {:git/url "https://github.com/lambdaisland/open-source" 3 | :git/sha "7ce125cbd14888590742da7ab3b6be9bba46fc7a"} 4 | current/project {:local/root "."}} 5 | 6 | :tasks 7 | {test:clj {:doc "Run Clojure JVM tests" 8 | :task (shell "bin/kaocha")} 9 | 10 | test:bb {:doc "Run babashka tests" 11 | :extra-paths ["test"] 12 | :extra-deps {org.clojure/test.check {:mvn/version "1.1.1"}} 13 | :requires ([lambdaisland.uri-test]) 14 | :task (let [{:keys [error fail]} (clojure.test/run-tests 'lambdaisland.uri-test)] 15 | (when (pos? (+ error fail)) 16 | (throw (ex-info "Tests failed" {:babashka/exit 1}))))}}} 17 | -------------------------------------------------------------------------------- /bin/kaocha: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [[ -d "node_modules/ws" ]] || npm install ws 4 | 5 | clojure -A:dev:test -m kaocha.runner "$@" 6 | -------------------------------------------------------------------------------- /bin/proj: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bb 2 | 3 | (ns proj (:require [lioss.main :as lioss])) 4 | 5 | (lioss/main 6 | {:license :mpl 7 | :group-id "lambdaisland" 8 | :inception-year 2017 9 | :description "A pure Clojure/ClojureScript URI library."}) 10 | 11 | 12 | ;; Local Variables: 13 | ;; mode:clojure 14 | ;; End: 15 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {org.clojure/clojure {:mvn/version "1.11.1"}} 3 | 4 | :aliases 5 | {:dev {} 6 | :test 7 | {:extra-paths ["test"] 8 | :extra-deps 9 | {org.clojure/clojurescript {:mvn/version "1.11.121"} 10 | lambdaisland/kaocha {:mvn/version "1.87.1366"} 11 | com.lambdaisland/kaocha-cljs {:mvn/version "1.5.154"} 12 | lambdaisland/kaocha-junit-xml {:mvn/version "1.17.101"} 13 | org.clojure/test.check {:mvn/version "1.1.1"}}}}} 14 | -------------------------------------------------------------------------------- /generate_docs: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export AUTODOC_CMD="lein with-profile +codox codox" 4 | 5 | \curl -sSL https://raw.githubusercontent.com/plexus/autodoc/master/autodoc.sh | bash 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "ws": "^8.16.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | lambdaisland 5 | uri 6 | 1.19.155 7 | uri 8 | A pure Clojure/ClojureScript URI library. 9 | https://github.com/lambdaisland/uri 10 | 2017 11 | 12 | Lambda Island 13 | https://lambdaisland.com 14 | 15 | 16 | UTF-8 17 | 18 | 19 | 20 | MPL-2.0 21 | https://www.mozilla.org/media/MPL/2.0/index.txt 22 | 23 | 24 | 25 | https://github.com/lambdaisland/uri 26 | scm:git:git://github.com/lambdaisland/uri.git 27 | scm:git:ssh://git@github.com/lambdaisland/uri.git 28 | 1b0a2d0d6845af5dd45d3f8685d1db9a20ca56ef 29 | 30 | 31 | 32 | org.clojure 33 | clojure 34 | 1.11.1 35 | 36 | 37 | 38 | src 39 | 40 | 41 | src 42 | 43 | 44 | 45 | 46 | org.apache.maven.plugins 47 | maven-compiler-plugin 48 | 3.8.1 49 | 50 | 1.8 51 | 1.8 52 | 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-jar-plugin 57 | 3.2.0 58 | 59 | 60 | 61 | 1b0a2d0d6845af5dd45d3f8685d1db9a20ca56ef 62 | 63 | 64 | 65 | 66 | 67 | org.apache.maven.plugins 68 | maven-gpg-plugin 69 | 1.6 70 | 71 | 72 | sign-artifacts 73 | verify 74 | 75 | sign 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | clojars 85 | https://repo.clojars.org/ 86 | 87 | 88 | 89 | 90 | clojars 91 | Clojars repository 92 | https://clojars.org/repo 93 | 94 | 95 | -------------------------------------------------------------------------------- /repl_sessions/encode.clj: -------------------------------------------------------------------------------- 1 | (ns encode 2 | (:require [lambdaisland.uri :as uri] 3 | [lambdaisland.uri.normalize :as normalize])) 4 | 5 | (-> (uri/uri "https://example.com/") 6 | (assoc :query (str "src=" (normalize/percent-encode "/foo/bar" :query))) 7 | (uri/assoc-query :frame-id "frame-1d3ba257-4c55-48a5-8011-5d1aba5c240f" :kind "iframe") 8 | str) 9 | "https://example.com/?src=/foo/bar&kind=iframe&frame-id=frame-1d3ba257-4c55-48a5-8011-5d1aba5c240f" 10 | "https://example.com/?src=/foo/bar&kind=iframe&frame-id=frame-1d3ba257-4c55-48a5-8011-5d1aba5c240f" 11 | 12 | (-> (uri/uri "https://example.com/") 13 | (assoc :query (str "src=" (normalize/percent-encode "/foo/bar" :query))) 14 | str) 15 | ;;=> "https://example.com/?src=/foo/bar" 16 | 17 | (str domain "/a/iframe-viewer?src=" (url/url-encode src) 18 | "&frame-id=" frame-id "&kind=" (name kind)) 19 | 20 | (-> (uri/uri domain) 21 | (assoc :path "/a/iframe-viewer") 22 | (assoc-query :src src :frame-id frame-id :kind (name kind)) 23 | str) 24 | 25 | (normalize/normalize (uri/uri "https://dev.nextjournalusercontent.com:8888/a/iframe-viewer?src=%2Fdata%2FQmfFEyfAfGq3siNJyVxMcddyxtHtRjFKqfm81Nw5wiML3V%3Fcontent-type%3Dapplication%252Fvnd.nextjournal.html%252Bhtml&frame-id=frame-1d3ba257-4c55-48a5-8011-5d1aba5c240f&kind=iframe")) 26 | 27 | #lambdaisland/uri "https://dev.nextjournalusercontent.com:8888/a/iframe-viewer?src=/data/QmfFEyfAfGq3siNJyVxMcddyxtHtRjFKqfm81Nw5wiML3V?content-type=application%252Fvnd.nextjournal.html%252Bhtml&frame-id=frame-1d3ba257-4c55-48a5-8011-5d1aba5c240f&kind=iframe" 28 | 29 | 30 | 31 | (-> (uri/uri "https://example.com/") 32 | (uri/assoc-query :xxx "?#") 33 | (uri/assoc-query :yyy "?#") 34 | (uri/assoc-query :zzz "?#&=") 35 | str) 36 | 37 | "https://example.com/?src=/foo/bar&kind=iframe&frame-id=frame-1d3ba257-4c55-48a5-8011-5d1aba5c240f" 38 | 39 | (-> (uri/uri "https://example.com/") 40 | (assoc :query (str "src=" (normalize/percent-encode "/foo/bar" :query))) 41 | str) 42 | "https://example.com/?src=/foo/bar" 43 | "https://example.com/?src=%2Ffoo%2Fbar" 44 | 45 | 46 | (uri/query-map (uri/uri "?foo=%20%2B%26xxx%3D123")) 47 | ;;=> {:foo " +&xxx=123"} 48 | (uri/query-map (normalize/normalize (uri/uri "?foo=%20%2B%26xxx%3D123"))) 49 | ;;=> {:foo " +", :xxx "123"} 50 | 51 | (-> (uri/uri "?foo=%20%2B%26xxx%3D123") 52 | normalize/normalize) 53 | #lambdaisland/uri "?foo=%20+&xxx=123" 54 | 55 | 56 | (normalize/percent-encode "+" :query) 57 | 58 | ;; => {:scheme "https" 59 | ;; :user nil 60 | ;; :password nil 61 | ;; :host "example.com" 62 | ;; :port nil 63 | ;; :path "/" 64 | ;; :query "" 65 | ;; :fragment "hello"} 66 | 67 | ;; => {:scheme "https" 68 | ;; :user nil 69 | ;; :password nil 70 | ;; :host "example.com" 71 | ;; :port nil 72 | ;; :path "/" 73 | ;; :query "#hello" 74 | ;; :fragment nil} 75 | 76 | Parameter id must conform to #"^\d+$", but got "%2Fdata%2FQmfFEyfAfGq3siNJyVxMcddyxtHtRjFKqfm81Nw5wiML3V%3Fcontent-type%3Dapplication%2Fvnd.nextjournal.html%2Bhtml" . 77 | Parameter id must conform to clojure.core/uuid? , but got "%2Fdata%2FQmfFEyfAfGq3siNJyVxMcddyxtHtRjFKqfm81Nw5wiML3V%3Fcontent-type%3Dapplication%2Fvnd.nextjournal.html%2Bhtml" . 78 | Parameter id must conform to com.nextjournal.journal.params/matches-base58? , but got "%2Fdata%2FQmfFEyfAfGq3siNJyVxMcddyxtHtRjFKqfm81Nw5wiML3V%3Fcontent-type%3Dapplication%2Fvnd.nextjournal.html%2Bhtml" . 79 | Parameter id must conform to com.nextjournal.journal.params/matches-uuid? , but got "%2Fdata%2FQmfFEyfAfGq3siNJyVxMcddyxtHtRjFKqfm81Nw5wiML3V%3Fcontent-type%3Dapplication%2Fvnd.nextjournal.html%2Bhtml" . 80 | 81 | https://dev.nextjournalusercontent.com:8888/a/ 82 | 83 | (-> (uri/uri "https://example.com") 84 | (assoc :path "/a/iframe-viewer") 85 | (uri/assoc-query :frame-id 123 86 | :kind "iframe" 87 | :src "/data/QmfFEyfAfGq3siNJyVxMcddyxtHtRjFKqfm81Nw5wiML3V?content-type=application/vnd.nextjournal.html+html") 88 | ) 89 | #lambdaisland/uri "https://example.com/a/iframe-viewer?src=/data/QmfFEyfAfGq3siNJyVxMcddyxtHtRjFKqfm81Nw5wiML3V%3Fcontent-type%3Dapplication/vnd.nextjournal.html+html&kind=iframe&frame-id=123" 90 | -------------------------------------------------------------------------------- /repl_sessions/merge_join.clj: -------------------------------------------------------------------------------- 1 | (require '[lambdaisland.uri :as uri]) 2 | 3 | (-> (uri/uri "https://my.service/some-context/?api_key=123") 4 | (update :path str "some-resource")) 5 | 6 | (defn uri-merge [& uris] 7 | (let [uri-maps (->> uris (map uri/uri) (map #(into {} (filter (comp some? val)) %))) 8 | merged-path (some->> uris (keep :path) seq (apply uri/join) :path)] 9 | (-> (apply merge uri-maps) 10 | (assoc :path merged-path) 11 | uri/map->URI))) 12 | 13 | (str (uri-merge "http://localhost:3303/hello-world" 14 | "https://en.wikipedia.org/wiki/VT100#Variants")) 15 | ;; => "https://en.wikipedia.org:3303#Variants" 16 | 17 | (def endpoint "https://my.service/some-context/") 18 | (def api-key "123") 19 | 20 | (defn api-uri [& segments] 21 | (-> (apply uri/join endpoint segments) 22 | (assoc :query (str "api-key=123")))) 23 | 24 | (api-uri endpoint "some-resource") 25 | ;; => "https://my.service/some-context/some-resource?api-key=123" 26 | -------------------------------------------------------------------------------- /repl_sessions/poke.clj: -------------------------------------------------------------------------------- 1 | (ns repl-sessions.poke 2 | (:require [lambdaisland.uri :as uri] 3 | [lambdaisland.uri.normalize :as norm])) 4 | 5 | (:fragment (norm/normalize (uri/map->URI {:fragment "'#'schön"}))) 6 | "'%23'sch%C3%B6n" 7 | 8 | (norm/normalize-fragment "schön") 9 | ;; => "sch%C3%B6n" 10 | 11 | 12 | (uri/map->query-string {:foo "bar"});; => "foo=bar" 13 | (uri/map->query-string {:foo/baz "bar"});; => "foo/baz=bar" 14 | (uri/query-string->map "foo=bar");; => {:foo "bar"} 15 | (uri/query-string->map "%3Afoo=bar" {:keywordize? false});; => {":foo" "bar"} 16 | 17 | ;; => #::foo{:baz "bar"} 18 | 19 | ;; => #:foo{:baz "bar"} 20 | 21 | (let [{:keys [scheme host port]} (uri/uri "https://foo.bar:8080")] 22 | ) 23 | 24 | (:query (uri/uri "http://example.com?hello")) 25 | 26 | (uri/map->query-string {:foo "", :bar nil}) 27 | ;; => "foo=" 28 | -------------------------------------------------------------------------------- /repl_sessions/round_trip_query_map.clj: -------------------------------------------------------------------------------- 1 | (ns round-trip-query-map 2 | (:require [lambdaisland.uri :as uri] 3 | [lambdaisland.uri.normalize :as norm] 4 | [lambdaisland.uri.platform :as platform] 5 | [clojure.test.check.generators :as gen] 6 | [clojure.test.check.properties :as prop] 7 | [clojure.test.check :as check] 8 | [clojure.string :as str])) 9 | 10 | (def query-map-gen 11 | (gen/map gen/keyword 12 | gen/string)) 13 | 14 | (check/quick-check 10000 15 | (prop/for-all [q query-map-gen] 16 | (let [res (-> q 17 | uri/map->query-string 18 | uri/query-string->map)] 19 | (or (and (empty? q) (empty? res)) ;; (= nil {}) 20 | (= q res))))) 21 | 22 | (def query-string-part-gen (gen/such-that (complement (partial some #{\& \= \% \space})) gen/string)) 23 | (def query-string-gen (gen/fmap 24 | (fn [parts] 25 | (str/join 26 | "&" 27 | (map (fn [[k v]] (str k "=" v)) parts))) 28 | (gen/list (gen/tuple (gen/such-that seq query-string-part-gen) 29 | (gen/such-that seq query-string-part-gen))))) 30 | 31 | (check/quick-check 100 32 | (prop/for-all [q query-string-gen] 33 | (let [res (-> q 34 | uri/query-string->map 35 | uri/map->query-string 36 | norm/percent-decode)] 37 | (or (and (empty? q) (empty? res)) ;; (= nil {}) 38 | (= q res))))) 39 | {:shrunk {:total-nodes-visited 327, :depth 36, :pass? false, :result false, :result-data nil, :time-shrinking-ms 65, :smallest ["=&=&="]}, :failed-after-ms 3, :num-tests 13, :seed 1592289824964, :fail ["£pÓn=Szþ«Ÿs&\f“ɽ¼=wÅýˆ¤‘Ôµ-&È-¼\t½?óø=µ\r÷2¶+’T&-šô•¤+=`MZ—R1-àò!&ÎÉڍÝ=©×DžÂ&¥=Ø#$‡/\\j‡Q&å=¯]3³­ñ˜&Û¼Ô=ãi™–´p‰e&‚Í=Päzi#V*Wäã"], :result false, :result-data nil, :failing-size 12, :pass? false} 40 | 41 | (-> x 42 | uri/map->query-string 43 | uri/query-string->map) 44 | 45 | (seq (platform/string->byte-seq "\u0000")) 46 | 47 | (map long (.getBytes "\b")) 48 | 49 | (-> "\u0000=\u0000&\u0001=\u0000&\u0000=\u0000" 50 | uri/query-string->map 51 | uri/map->query-string 52 | norm/percent-decode ) 53 | 54 | (norm/percent-encode "\b") 55 | 56 | (platform/byte->hex 16) 57 | 58 | (uri/map->query-string (uri/query-string->map "x=a+b")) 59 | -------------------------------------------------------------------------------- /repl_sessions/uri_url_file_path.clj: -------------------------------------------------------------------------------- 1 | (ns repl-sessions.uri-url-file-path 2 | (:require 3 | [clojure.java.io :as io]) 4 | (:import 5 | (java.io File) 6 | (java.net URI URL) 7 | (java.nio.file Files OpenOption Path Paths))) 8 | 9 | ;; Let's explore some JVM alternatives for dealing with URI/URL/File objects 10 | 11 | ;; There are four objects in the Java Platform for representing either file paths 12 | ;; or URI/URLs. 13 | 14 | ;; Two have been around all the way since Java 1.0. The other two came a bit later 15 | ;; - java.io.File : java 1.0 (1996) 16 | ;; - java.net.URL : java 1.0 (1996) 17 | ;; - java.net.URI : java 1.4 (2002) 18 | ;; - java.nio.file.Path : java 7 (2011) 19 | 20 | (File. "/tmp/xxx") 21 | (io/file "/tmp/xxx") 22 | (URL. "file:/tmp/xxx") 23 | (URI. "file:/tmp/xxx") 24 | (Path/of "/tmp/xxx" (into-array String [])) 25 | 26 | ;; # File 27 | 28 | ;; Internally consists of 29 | ;; - String path 30 | ;; - int pathPrefixLength 31 | ;; - char pathSeparatorChar 32 | 33 | ;; private static final FileSystem fs = DefaultFileSystem.getFileSystem(); 34 | ;; -> hidden but e.g. UNIXFileSystem 35 | ;; public static final char pathSeparatorChar = fs.getPathSeparator(); 36 | 37 | ;; OS-dependent 38 | (.getName (File. "C:\\hello\\world")) 39 | (.getName (File. "/foo/bar")) 40 | (.getParent (File. "/foo/bar")) 41 | 42 | ;; can deal with relative 43 | (.getCanonicalFile (File. "../foo/bar")) 44 | 45 | ;; most operations are delegated to the FileSystem implementation 46 | (.equals (io/file "hello") (io/file "HELLO")) 47 | ;; => on Windows this will return true 48 | 49 | 50 | ;;; # URL 51 | ;; Universal Resource Locator 52 | 53 | ;; Main fields: protocol / host / port / "file" 54 | ;; Many on-demand computed fields 55 | ;; - ref 56 | ;; - authority 57 | ;; - query 58 | ;; - path 59 | 60 | ;; - InetAddress 61 | ;; - URLStreamHandler 62 | 63 | (slurp (.openStream (URL. "https://clojure.org"))) 64 | (slurp (URL. "https://clojure.org")) 65 | 66 | ;; Built-in handlers 67 | ;; - file 68 | ;; - ftp 69 | ;; - http 70 | ;; - https 71 | ;; - jar 72 | ;; - jmod 73 | ;; - jrt 74 | ;; - mailto 75 | 76 | ;; Here be dragons 77 | 78 | (= (URL. "http://a.example.com/foo") 79 | (URL. "http://b.example.com/foo")) 80 | ;; -> does DNS resolution (so does .hashCode!) 81 | (= (URL. "http://example.com:80/foo") 82 | (URL. "http://example.com/foo")) 83 | 84 | ;; Compare 85 | (= (URI. "http://example.com:80/foo") 86 | (URI. "http://example.com/foo")) 87 | (= (URI. "http://a.example.com/foo") 88 | (URI. "http://b.example.com/foo")) 89 | 90 | ;; But, still widely used, many Java APIs take URL, and probably will forever 91 | ;; take URLs. 92 | (io/resource "clojure/core.clj") 93 | 94 | ;; # URI 95 | ;; Sensible! Simple value object! 96 | ;; Most of the code is for parsing, normalizing, relativizing (like lambdaisland.uri) 97 | ;; Can easily convert between the different forms. 98 | ;; Use this one if you can. (even when just working with files/paths) 99 | 100 | 101 | ;; # Path 102 | ;; Similar to File it relies on a FileSystemProvider (instead of a FileSystem) 103 | ;; But, not limited to one per system! 104 | ;; Can be third party! 105 | 106 | ;; Can be created with simple strings, will default to plain file. Beware: 107 | ;; varargs. 108 | 109 | (Path/of "hello" (into-array String [])) 110 | 111 | ;; Really neat that you can do this: 112 | (Files/write 113 | (Paths/get (URI. "gs://does-data-prod/sessionize/7171/workshop.edn")) 114 | (.getBytes "hello" "UTF-8") 115 | (into-array OpenOption [])) 116 | 117 | ;; Annoying this is making sure the filesystem is "open" 118 | -------------------------------------------------------------------------------- /repl_sessions/walkthrough.clj: -------------------------------------------------------------------------------- 1 | (ns repl-sessions.walkthrough 2 | "Walkthrough of the main features of lambdaisland.uri" 3 | ;; The public API is spread across these two namespaces 4 | (:require [lambdaisland.uri :as uri] 5 | [lambdaisland.uri.normalize :as normalize])) 6 | 7 | ;; tl;dr : CLJC (Clojure+ClojureScript) library for parsing, creating, 8 | ;; manipulating URIs, with an API that leans into simple `clojure.core` 9 | ;; functions. 10 | 11 | ;; The `uri/uri` constructor returns a `lambdaisland.uri.URI`, which is a 12 | ;; Clojure record 13 | 14 | (def u (uri/uri "https://lambdaisland.com/episodes/all")) 15 | 16 | (type u) 17 | (record? u) 18 | 19 | ;; Since it's a record, it acts like a map, so you can access and manipulate the 20 | ;; individual parts of the URL using plain Clojure map functions, like `get`, 21 | ;; `assoc`, etc. 22 | 23 | ;; Use `into` to convert to a plain map, so you can see what's inside 24 | (into {} (uri/uri "https://lambdaisland.com/episodes/all")) 25 | (into {} (uri/uri "https://arne:secret@lambdaisland.com:123/episodes/all?hello#boo")) 26 | ;; => {:scheme "https", 27 | ;; :user nil, 28 | ;; :password nil, 29 | ;; :host "lambdaisland.com", 30 | ;; :port nil, 31 | ;; :path "/episodes/all", 32 | ;; :query nil, 33 | ;; :fragment nil} 34 | 35 | ;; This provides for a very natural API, just use clojure.core 36 | (:host u) 37 | (:path u) 38 | (assoc u :fragment "hello") 39 | (assoc u :path "") 40 | 41 | ;; lambdaisland.uri.URI implements Object#toString, so converting it to a String 42 | ;; reconstitutes the URI 43 | (str u) 44 | (-> u 45 | (assoc :path "/about") 46 | (assoc :query "page=1") 47 | str) 48 | 49 | ;; Calls to `uri/uri` are idempotent 50 | (uri/uri u) 51 | 52 | ;; We coerce any argument to string first, which means many other 53 | ;; representations "just work" when passed to `uri/uri` 54 | (uri/uri (java.net.URI. "http://example.com")) 55 | 56 | ;; We implement the RFC3986 algorithm for joining (resolving) URI based on a 57 | ;; base URI. E.g. this is what a browser does when you follow a relative link. 58 | (def base "http://example.com/api/v1/") 59 | 60 | (uri/join base "hello/world") 61 | (uri/join base "/hello/world") 62 | (uri/join base "./hello/../../world") 63 | 64 | ;; `uri/uri` considers the "query" part of a URL as one thing, because really 65 | ;; you can shove anything in there 66 | 67 | (:query (uri/uri "http://example.com/?xyz123")) 68 | 69 | ;; But it's common to treat it as key-value pairs, we have helpers to do just 70 | ;; that. 71 | (uri/query-map "http://example.com/?foo=bar&hello=world") 72 | 73 | ;; Note that these automatically coerce their first argument if necessary 74 | (-> "http://example.com/?foo=bar&hello=world" 75 | (uri/assoc-query :foo 123)) 76 | 77 | (-> "http://example.com/?foo=bar&hello=world" 78 | (uri/assoc-query :foo [123 456])) 79 | 80 | (-> "http://example.com/?foo=bar&hello=world" 81 | (uri/assoc-query :foo "foo bar")) 82 | 83 | ;; This probably should've been called `merge-query`, oh well. 84 | (-> "http://example.com/?foo=bar&hello=world" 85 | (uri/assoc-query* {:foo "foo bar"})) 86 | 87 | ;; When "parsing" a URI we don't do any percent decoding, we assume that your 88 | ;; URI is valid, i.e. that any invalid characters are percent-encoded 89 | (uri/uri "http://example.com/%61%62%63") 90 | 91 | ;; `normalize` will encode what needs encoding, and decode what needs decoding. 92 | ;; This correctly handles "surrogate pairs" in Java/JavaScript 93 | ;; strings (characters that have a codepoint that doesn't fit in 16 bits). 94 | (normalize/normalize (uri/uri "http://example.com/🙈🙉")) 95 | (normalize/normalize (uri/uri "http://example.com/%61%62%63")) 96 | 97 | ;; Note that percent-encoding is dependent on the part of the URI you're dealing 98 | ;; with. 99 | (normalize/normalize (uri/uri "http://example.com/foo = bar")) 100 | (normalize/normalize (uri/assoc-query "http://example.com/" :foo " =bar= ")) 101 | 102 | ;; And that sometimes there's a semantic difference between percent and not 103 | ;; percent encoded 104 | 105 | ;; In a Java context you sometimes find "nested" URIs, with pseudo-schemes like 106 | ;; `jdbc` or `jar`, here you might have to re-parse the `:path` of the outer URI. 107 | (:scheme 108 | (uri/uri 109 | (:path 110 | (uri/uri "jdbc:postgresql://localhost:5432/metabase?user=metabase&password=metabase")))) 111 | 112 | (str 113 | (assoc 114 | (uri/uri 115 | (:path 116 | (uri/uri "jdbc:postgresql://localhost:5432/metabase?user=metabase&password=metabase"))) 117 | :user "user" 118 | :password "pwd")) 119 | -------------------------------------------------------------------------------- /src/lambdaisland/uri.cljc: -------------------------------------------------------------------------------- 1 | (ns lambdaisland.uri 2 | (:refer-clojure :exclude [uri?]) 3 | (:require [clojure.string :as str] 4 | [lambdaisland.uri.normalize :as normalize]) 5 | #?(:clj (:import clojure.lang.IFn))) 6 | 7 | (def uri-regex #?(:clj #"\A(([^:/?#]+):)?(//([^/?#\\]*))?([^?#]*)?(\?([^#]*))?(#(.*))?\z" 8 | :cljs #"^(([^:/?#]+):)?(//([^/?#\\]*))?([^?#]*)?(\?([^#]*))?(#(.*))?$")) 9 | (def authority-regex #?(:clj #"\A(([^:]*)(:(.*))?@)?(\[[0-9a-fA-F:]+\]|[^:]*)(:(\d*))?\z" 10 | :cljs #"^(([^:]*)(:(.*))?@)?(\[[0-9a-fA-F:]+\]|[^:]*)(:(\d*))?$")) 11 | 12 | (defn- authority-string [user password host port] 13 | (when host 14 | (cond-> user 15 | (and user password) (str ":" password) 16 | user (str "@") 17 | true (str host) 18 | port (str ":" port)))) 19 | 20 | (defn uri-str 21 | "Convert the URI instance back to a string" 22 | [{:keys [scheme user password host port path query fragment]}] 23 | (let [authority (authority-string user password host port)] 24 | (cond-> "" 25 | scheme (str scheme ":") 26 | authority (str "//" authority) 27 | true (str path) 28 | query (str "?" query) 29 | fragment (str "#" fragment)))) 30 | 31 | (defrecord URI [scheme user password host port path query fragment] 32 | #?@(:bb [] 33 | :default 34 | [IFn 35 | (#?(:clj invoke :cljs -invoke) [this kw] 36 | (get this kw))]) 37 | Object 38 | (toString [this] 39 | (uri-str this))) 40 | 41 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 42 | ;; parse 43 | 44 | (defn- match-uri [uri] 45 | (let [matches (re-matches uri-regex uri) 46 | [_ _ scheme _ authority path _ query _ fragment] matches] 47 | [scheme authority (when (seq path) path) query fragment])) 48 | 49 | (defn- match-authority [authority] 50 | (let [matches (re-matches authority-regex authority) 51 | [_ _ user _ password host _ port] matches] 52 | [user password host port])) 53 | 54 | (defn parse 55 | "Parse a URI string into a lambadisland.uri.URI record." 56 | [uri] 57 | (let [[scheme authority path query fragment] (match-uri uri)] 58 | (if authority 59 | (let [[user password host port] (match-authority authority)] 60 | (URI. scheme user password host port path query fragment)) 61 | (URI. scheme nil nil nil nil path query fragment)))) 62 | 63 | (defn uri 64 | "Turn the given value into a lambdaisland.uri.URI record, if it isn't one 65 | already. Supports String, java.net.URI, and other URI-like objects that return 66 | a valid URI string with `str`." 67 | [uri-like] 68 | (if (instance? URI uri-like) 69 | uri-like 70 | (parse (str uri-like)))) 71 | 72 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 73 | ;; join / resolve 74 | 75 | ;; This section is based on RFC 3986 76 | 77 | (defn- absolute-path? [path] 78 | (= (first path) \/)) 79 | 80 | (defn- remove-dot-segments 81 | "As per RFC 3986 section 5.2.4" 82 | [path] 83 | (when path 84 | (loop [in (str/split path #"(?=/)") 85 | out []] 86 | (case (first in) 87 | "/." (if (next in) 88 | (recur (next in) out) 89 | (recur nil (conj out "/"))) 90 | "/.." (if (next in) 91 | (recur (next in) (vec (butlast out))) 92 | (recur nil (conj (vec (butlast out)) "/"))) 93 | nil (str/join out) 94 | (recur (next in) (conj out (first in))))))) 95 | 96 | (defn- merge-paths [a b] 97 | (if (some #{\/} a) 98 | (str (re-find #?(:clj #"\A.*/" 99 | :cljs #"^.*/") a) b) 100 | (if (absolute-path? b) 101 | b 102 | (str "/" b)))) 103 | 104 | (defn join* 105 | "Join two URI records as per RFC 3986. Handles relative URIs." 106 | [base ref] 107 | (if (:scheme ref) 108 | (update ref :path remove-dot-segments) 109 | (-> (if (:host ref) 110 | (assoc ref 111 | :scheme (:scheme base) 112 | :query (:query ref)) 113 | (if (nil? (:path ref)) 114 | (assoc base :query (some :query [ref base])) 115 | (assoc base :path 116 | (remove-dot-segments 117 | (if (absolute-path? (:path ref)) 118 | (:path ref) 119 | (merge-paths (:path base) (:path ref)))) 120 | :query (:query ref)))) 121 | (assoc :fragment (:fragment ref))))) 122 | 123 | (defn join 124 | "Joins any number of URIs as per RFC3986. Arguments can be strings, they will 125 | be coerced to URI records." 126 | [& uris] 127 | (reduce join* (map uri uris))) 128 | 129 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 130 | ;; Query strings 131 | 132 | (defn- decode-param-pair [param] 133 | (let [[k v] (str/split param #"=" 2)] 134 | [(if k (normalize/percent-decode k) "") 135 | (if v (normalize/percent-decode (str/replace v #"\+" " ")) "")])) 136 | 137 | (defn query-string->map 138 | "Parse a query string, consisting of key=value pairs, separated by \"&\". Takes 139 | the following options: 140 | 141 | - `:keywordize?` whether to turn return keys as keywords. Defaults to `true`. 142 | - `:multikeys` how to handle the same key occuring multiple times, defaults to 143 | `:duplicates` 144 | - `:into` provide `clojure.lang.IPersistentMap` to define target data structure, 145 | defaults to `clojure.lang.PersistentHashMap` 146 | 147 | The possible values for `:multikeys` are 148 | 149 | - `:never` always return a single value for a key. The rightmost value 150 | \"wins\" 151 | - `:always` return a map with vectors as values, with successive 152 | values of the same key in order 153 | - `:duplicates` return a vector for keys that occur multiple times, or a 154 | string otherwise" 155 | ([q] 156 | (query-string->map q nil)) 157 | ([q {:keys [multikeys keywordize? into] 158 | :or {multikeys :duplicates 159 | keywordize? true}}] 160 | (if (str/blank? q) 161 | into 162 | (->> (str/split q #"&") 163 | (map decode-param-pair) 164 | (reduce 165 | (fn [m [k v]] 166 | (let [k (if keywordize? (keyword k) k)] 167 | (case multikeys 168 | :never 169 | (assoc m k v) 170 | :always 171 | (if (contains? m k) 172 | (update m k conj v) 173 | (assoc m k [v])) 174 | :duplicates 175 | (if (contains? m k) 176 | (if (vector? (m k)) 177 | (update m k conj v) 178 | (assoc m k [(m k) v])) 179 | (assoc m k v))))) 180 | (or into {})))))) 181 | 182 | (defn query-string->seq 183 | "Parse a query string, consisting of key=value pairs, separated by `\"&\"`. 184 | Return a seq of `[key value]` pairs, in the exact order they occur in the 185 | query string. Keys and values are strings. Returns `nil` if `q` is blank or 186 | `nil`." 187 | [q] 188 | (when-not (str/blank? q) 189 | (map decode-param-pair (str/split q #"&")))) 190 | 191 | (defn query-map 192 | "Return the query section of a URI as a map. Will coerce its argument 193 | with [[uri]]. Takes an options map, see [[query-string->map]] for options." 194 | ([uri] 195 | (query-map uri nil)) 196 | ([u opts] 197 | (query-string->map (:query (uri u)) opts))) 198 | 199 | (defn query-encode 200 | "Percent encoding for query strings. Will percent-encode values that are 201 | reserved in query strings only. Encodes spaces as +." 202 | [s] 203 | (let [encode-char #(cond 204 | (= " " %) 205 | "+" 206 | (re-find #"[^a-zA-Z0-9\-\._~@\/]" %) 207 | (normalize/percent-encode %) 208 | :else 209 | %)] 210 | (->> (normalize/char-seq s) 211 | (map encode-char) 212 | (apply str)))) 213 | 214 | (defn- encode-param-pair [k v] 215 | (str (query-encode 216 | (cond 217 | (simple-ident? k) 218 | (name k) 219 | (qualified-ident? k) 220 | (str (namespace k) "/" (name k)) 221 | :else (str k))) 222 | "=" 223 | (query-encode (str v)))) 224 | 225 | (defn map->query-string 226 | "Convert a map into a query string, consisting of key=value pairs separated by 227 | `&`. The result is percent-encoded so it is always safe to use. Keys can be 228 | strings or keywords. If values are collections then this results in multiple 229 | entries for the same key. `nil` values are ignored. Values are stringified." 230 | [m] 231 | (when (seq m) 232 | (->> m 233 | (mapcat (fn [[k v]] 234 | (cond 235 | (nil? v) 236 | [] 237 | (coll? v) 238 | (map (partial encode-param-pair k) v) 239 | :else 240 | [(encode-param-pair k v)]))) 241 | (interpose "&") 242 | (apply str)))) 243 | 244 | (defn seq->query-string 245 | "Convert a positional sequence of `[key value]` tuples into a query string, 246 | consisting of key=value pairs separated by `&`. The result is percent-encoded, 247 | so it is always safe to use. Keys can be strings or keywords. Tuples with 248 | `nil` values adds entries like `key=` to query string. Values are 249 | stringified." 250 | [pairs] 251 | (->> pairs 252 | (map (fn [[k v]] (encode-param-pair k v))) 253 | (interpose "&") 254 | (apply str))) 255 | 256 | (defn assoc-query* 257 | "Add additional query parameters to a URI. Takes a URI (or coercible to URI) and 258 | a map of query params." 259 | [u m] 260 | (let [u (uri u)] 261 | (assoc u :query (map->query-string (merge (query-map u) m))))) 262 | 263 | (defn assoc-query 264 | "Add additional query parameters to a URI. Takes a URI (or coercible to URI) 265 | followed key value pairs. 266 | 267 | (assoc-query \"http://example.com?id=1&name=John\" :name \"Jack\" :style \"goth\") 268 | ;;=> #lambdaisland/uri \"http://example.com?id=1&name=Jack&style=goth\" " 269 | [u & {:as kvs}] 270 | (assoc-query* u kvs)) 271 | 272 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 273 | ;; Predicates 274 | 275 | (defn relative? 276 | "Is the URI relative? Returns true if the URI does not have a scheme (protocol)." 277 | [uri] 278 | (nil? (:scheme uri))) 279 | 280 | (def 281 | ^{:doc 282 | "Is the URI absolute? Returns true if the URI has a scheme (protocol), and hence also an origin."} 283 | absolute? (complement relative?)) 284 | 285 | (defn uri? 286 | "Check if `o` is URI instance." 287 | [o] 288 | (instance? URI o)) 289 | 290 | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 291 | ;; EDN 292 | 293 | (def edn-tag 'lambdaisland/uri) 294 | 295 | #?(:clj 296 | (defmethod print-method URI [^URI this ^java.io.Writer writer] 297 | (.write writer "#") 298 | (.write writer (str edn-tag)) 299 | (.write writer " ") 300 | (.write writer (prn-str (.toString this)))) 301 | 302 | :cljs 303 | (extend-type URI 304 | IPrintWithWriter 305 | (-pr-writer [this writer _opts] 306 | (write-all writer "#" (str edn-tag) " " (prn-str (.toString this)))))) 307 | 308 | (def 309 | ^{:doc 310 | "A map that can be passed to clojure.edn/read, so tagged URI literals are 311 | read back correctly."} 312 | edn-readers {edn-tag parse}) 313 | -------------------------------------------------------------------------------- /src/lambdaisland/uri/normalize.cljc: -------------------------------------------------------------------------------- 1 | (ns lambdaisland.uri.normalize 2 | (:require [clojure.string :as str] 3 | [lambdaisland.uri.platform :refer [byte-seq->string 4 | string->byte-seq 5 | byte->hex hex->byte 6 | char-code-at 7 | str-len]])) 8 | 9 | ;; TODO we might be better off having these just be sets 10 | (def 11 | ^{:doc 12 | "Which characters should be percent-encoded depends on which section 13 | of the URI is being normalized. This map contains regexes that for each 14 | case match the characters that need encoding."} 15 | character-classes 16 | (let [alpha "a-zA-Z" 17 | digit "0-9" 18 | gen-delims ":\\/\\?#\\[\\]@" 19 | sub-delims "!\\$&'\\(\\)\\*\\+,;=" 20 | reserved (str gen-delims sub-delims) 21 | unreserved (str alpha digit "\\-\\._~") 22 | pchar (str unreserved sub-delims ":@") 23 | scheme (str alpha digit "\\-\\+\\.") 24 | host (str unreserved sub-delims "\\[:\\]") 25 | authority pchar 26 | path (str pchar "\\/") 27 | query (str unreserved ":@\\/\\?") 28 | fragment (str pchar "\\/\\?")] 29 | {:alpha (re-pattern (str "[^" alpha "]")) 30 | :digit (re-pattern (str "[^" digit "]")) 31 | :gen-delims (re-pattern (str "[^" gen-delims "]")) 32 | :sub-delims (re-pattern (str "[^" sub-delims "]")) 33 | :reserved (re-pattern (str "[^" reserved "]")) 34 | :unreserved (re-pattern (str "[^" unreserved "]")) 35 | :pchar (re-pattern (str "[^" pchar "]")) 36 | :scheme (re-pattern (str "[^" scheme "]")) 37 | :host (re-pattern (str "[^" host "]")) 38 | :authority (re-pattern (str "[^" authority "]")) 39 | :path (re-pattern (str "[^" path "]")) 40 | :query (re-pattern (str "[^" query "]")) 41 | :fragment (re-pattern (str "[^" fragment "]"))})) 42 | 43 | (defn high-surrogate? [char-code] 44 | (<= 0xD800 char-code 0xDBFF)) 45 | 46 | (defn char-seq 47 | "Return a seq of the characters in a string, making sure not to split up 48 | UCS-2 (or is it UTF-16?) surrogate pairs. Because JavaScript. And Java." 49 | ([str] 50 | (char-seq str 0)) 51 | ([str offset] 52 | (loop [offset offset 53 | res []] 54 | (if (>= offset (str-len str)) 55 | res 56 | (let [code (char-code-at str offset) 57 | width (if (high-surrogate? code) 2 1) 58 | next-offset (+ offset width) 59 | cur-char (subs str offset next-offset)] 60 | (recur next-offset 61 | (conj res cur-char))))))) 62 | 63 | (defn percent-encode 64 | "Convert characters in their percent encoded form. e.g. 65 | `(percent_encode \"a\") #_=> \"%61\"`. When given a second argument, then 66 | only characters of the given character class are encoded, 67 | see `character-class`. 68 | 69 | Characters are encoded as UTF-8. To use a different encoding. re-bind 70 | `*character-encoding*`" 71 | ([component] 72 | (->> (string->byte-seq component) 73 | (map #(str "%" (byte->hex %))) 74 | (apply str))) 75 | ([component type] 76 | (let [char-class (get character-classes type) 77 | encode-char #(cond-> % (re-find char-class %) percent-encode)] 78 | (->> (char-seq component) 79 | (map encode-char) 80 | (apply str))))) 81 | 82 | (defn percent-decode 83 | "The inverse of `percent-encode`, convert any %XX sequences in a string to 84 | characters. Byte sequences are interpreted as UTF-8. To use a different 85 | encoding. re-bind `*character-encoding*`" 86 | [s] 87 | (when s 88 | (str/replace s #"(%[0-9A-Fa-f]{2})+" 89 | (fn [[x _]] 90 | (byte-seq->string 91 | (->> (str/split x #"%") 92 | (drop 1) 93 | (map hex->byte))))))) 94 | 95 | (defn normalize-path [path] 96 | (when-not (nil? path) 97 | (percent-encode (percent-decode path) :path))) 98 | 99 | (defn normalize-fragment [fragment] 100 | (when-not (nil? fragment) 101 | (percent-encode (percent-decode fragment) :fragment))) 102 | 103 | (defn hex-code-point? [cp] 104 | (or (<= #_(long \0) 48 cp #_(long \9) 57) 105 | (<= #_(long \A) 65 cp #_(long \F) 70) 106 | (<= #_(long \a) 97 cp #_(long \f) 102))) 107 | 108 | (def sub-delims 109 | "RFC3986 section 2.2 110 | 111 | The purpose of reserved characters is to provide a set of delimiting 112 | characters that are distinguishable from other data within a URI. 113 | URIs that differ in the replacement of a reserved character with its 114 | corresponding percent-encoded octet are not equivalent. Percent- 115 | encoding a reserved character, or decoding a percent-encoded octet 116 | that corresponds to a reserved character, will change how the URI is 117 | interpreted by most applications. Thus, characters in the reserved 118 | set are protected from normalization and are therefore safe to be 119 | used by scheme-specific and producer-specific algorithms for 120 | delimiting data subcomponents within a URI. " 121 | #{"!" "$" "&" "'" "(" ")" "*" "+" "," ";" "="}) 122 | 123 | (defn normalize-query 124 | "Normalize the query section of a URI 125 | 126 | - sub-delimiters that are not percent encoded are left unencoded 127 | - sub-delimiters and other reserved characters are always percent encoded 128 | - non-reserved characters that are percent-encoded are decoded 129 | " 130 | [s] 131 | (when s 132 | (let [len (str-len s)] 133 | (loop [i 0 134 | res []] 135 | (cond 136 | (= i len) 137 | (apply str res) 138 | 139 | (and (< i (- len 2)) 140 | (= 37 (char-code-at s i)) 141 | (hex-code-point? (char-code-at s (inc i))) 142 | (hex-code-point? (char-code-at s (+ i 2)))) 143 | (recur (+ i 3) 144 | (conj res (percent-encode (percent-decode (subs s i (+ i 3))) 145 | :query))) 146 | 147 | (contains? sub-delims (subs s i (inc i))) 148 | (recur (inc i) 149 | (conj res (subs s i (inc i)))) 150 | 151 | :else 152 | (let [increment (if (high-surrogate? (char-code-at s i)) 2 1)] 153 | (recur (+ i increment) 154 | (conj res (percent-encode (subs s i (+ i increment)) :query))))))))) 155 | 156 | (defn normalize 157 | "Normalize a lambdaisland.uri.URI. Currently normalizes (percent-encodes) the 158 | path, query, and fragment." 159 | [uri] 160 | (-> uri 161 | (update :path normalize-path) 162 | (update :query normalize-query) 163 | (update :fragment normalize-fragment))) 164 | -------------------------------------------------------------------------------- /src/lambdaisland/uri/platform.clj: -------------------------------------------------------------------------------- 1 | (ns lambdaisland.uri.platform) ;; clj 2 | 3 | (defn string->byte-seq [^String s] 4 | (map #(if (< % 0) (+ % 256) %) (.getBytes s "UTF8"))) 5 | 6 | (defn byte-seq->string [arr] 7 | (String. (byte-array arr) "UTF8")) 8 | 9 | (defn hex->byte [hex] 10 | (Integer/parseInt hex 16)) 11 | 12 | 13 | (defn byte->hex [byte] 14 | {:pre [(<= 0 byte 255)]} 15 | (format "%02X" byte)) 16 | 17 | (defn char-code-at [^String str pos] 18 | (long ^Character (.charAt str pos))) 19 | 20 | (defn str-len [^String s] 21 | (.length s)) 22 | -------------------------------------------------------------------------------- /src/lambdaisland/uri/platform.cljs: -------------------------------------------------------------------------------- 1 | (ns lambdaisland.uri.platform ;; cljs 2 | (:require [goog.crypt :as c])) 3 | 4 | (defn string->byte-seq [s] 5 | (c/stringToUtf8ByteArray s)) 6 | 7 | (defn byte-seq->string [arr] 8 | (c/utf8ByteArrayToString (apply array arr))) 9 | 10 | (defn hex->byte [hex] 11 | (js/parseInt hex 16)) 12 | 13 | (def hex-digit {0 "0" 1 "1" 2 "2" 3 "3" 14 | 4 "4" 5 "5" 6 "6" 7 "7" 15 | 8 "8" 9 "9" 10 "A" 11 "B" 16 | 12 "C" 13 "D" 14 "E" 15 "F"}) 17 | 18 | (defn byte->hex [byte] 19 | (let [byte (bit-and 0xFF byte) 20 | low-nibble (bit-and 0xF byte) 21 | high-nibble (bit-shift-right byte 4)] 22 | (str (hex-digit high-nibble) (hex-digit low-nibble)))) 23 | 24 | (defn char-code-at [str pos] 25 | (.charCodeAt str pos)) 26 | 27 | (defn str-len [s] 28 | (.-length s)) 29 | -------------------------------------------------------------------------------- /test/lambdaisland/test_runner.cljs: -------------------------------------------------------------------------------- 1 | (ns lambdaisland.test-runner 2 | (:require [doo.runner :refer-macros [doo-tests]] 3 | [lambdaisland.uri-test] 4 | [lambdaisland.uri.normalize-test])) 5 | 6 | (doo-tests 'lambdaisland.uri-test 7 | 'lambdaisland.uri.normalize-test) 8 | -------------------------------------------------------------------------------- /test/lambdaisland/uri/normalize_test.cljc: -------------------------------------------------------------------------------- 1 | (ns lambdaisland.uri.normalize-test 2 | (:require [lambdaisland.uri :as uri] 3 | [lambdaisland.uri.normalize :as n] 4 | [clojure.test :refer [deftest testing is are]])) 5 | 6 | (deftest normalize-test 7 | (are [x y] (= (-> x uri/parse n/normalize str) y) 8 | "http://example.com/a b c" "http://example.com/a%20b%20c" 9 | "http://example.com/a%20b%20c" "http://example.com/a%20b%20c" 10 | "/𝍖" "/%F0%9D%8D%96" 11 | "http://foo.bar/?x=%20" "http://foo.bar/?x=%20" 12 | "https://example.com?text=You are welcome 🙂" "https://example.com?text=You%20are%20welcome%20%F0%9F%99%82" ) 13 | 14 | (are [x y] (= (-> x n/normalize str) y) 15 | (uri/map->URI {:query "x=y"}) "?x=y" 16 | (uri/map->URI {:query "x=?y#"}) "?x=?y%23" 17 | (uri/map->URI {:query "foo=bar"}) "?foo=bar" 18 | (uri/map->URI {:query "foo=b%61r"}) "?foo=bar" 19 | (uri/map->URI {:query "foo=bar%3Dbaz"}) "?foo=bar%3Dbaz" 20 | (uri/map->URI {:query "foo=%20%2B%26xxx%3D123"}) "?foo=%20%2B%26xxx%3D123" 21 | (uri/map->URI {:query "text=You are welcome 🙂"}) "?text=You%20are%20welcome%20%F0%9F%99%82" 22 | )) 23 | 24 | (deftest char-seq-test 25 | (let [long-string (->> "s" 26 | ;; Long enough to trigger StackOverflow in non-tail recursive cases. 27 | (repeat 5000) 28 | (apply str)) 29 | long-string-len (count long-string) 30 | cs (n/char-seq long-string)] 31 | (is (= long-string-len (count cs))) 32 | (is (every? #{"s"} cs)))) 33 | 34 | (deftest normalize-path-test 35 | (are [x y] (= (n/normalize-path x) y) 36 | "/abc" "/abc" 37 | "𝍖" "%F0%9D%8D%96")) 38 | 39 | (deftest percent-encode-test 40 | (are [class comp result] (= (n/percent-encode comp class) result) 41 | :alpha "abcAbc" "abcAbc" 42 | :alpha "abc123" "abc%31%32%33" 43 | :path "abc/123" "abc/123" 44 | :path "abc/123:/#" "abc/123:/%23" 45 | :path "𝍖" "%F0%9D%8D%96")) 46 | 47 | (deftest percent-decode-test 48 | (are [in out] (= (n/percent-decode in) out) 49 | "%61%62%63" "abc" 50 | "%F0%9F%99%88%F0%9F%99%89" "🙈🙉")) 51 | -------------------------------------------------------------------------------- /test/lambdaisland/uri_test.cljc: -------------------------------------------------------------------------------- 1 | (ns lambdaisland.uri-test 2 | (:require [clojure.test :as t :refer [are deftest is testing]] 3 | [lambdaisland.uri :as uri] 4 | [lambdaisland.uri.normalize :as norm] 5 | [lambdaisland.uri.platform :as platform] 6 | [clojure.test.check.generators :as gen] 7 | [clojure.test.check.properties :as prop] 8 | [clojure.test.check.clojure-test :as tc] 9 | [clojure.string :as str])) 10 | 11 | (deftest parsing 12 | (testing "happy path" 13 | (are [x y] (= y (uri/parse x)) 14 | "http://user:password@example.com:8080/path?query=value#fragment" 15 | (uri/->URI "http" "user" "password" "example.com" "8080" "/path" "query=value" "fragment") 16 | 17 | "/happy/path" 18 | (uri/->URI nil nil nil nil nil "/happy/path" nil nil) 19 | 20 | "relative/path" 21 | (uri/->URI nil nil nil nil nil "relative/path" nil nil) 22 | 23 | "http://example.com" 24 | (uri/->URI "http" nil nil "example.com" nil nil nil nil) 25 | 26 | "http://[fe80::4ecc:6aff::00aa]:8080" 27 | (uri/->URI "http" nil nil "[fe80::4ecc:6aff::00aa]" "8080" nil nil nil)))) 28 | 29 | (deftest joining 30 | (are [x y] (= (uri/parse y) (apply uri/join (map uri/parse x))) 31 | ["http://foo.bar" "https://baz.com"] "https://baz.com" 32 | ["http://example.com" "/a/path"] "http://example.com/a/path" 33 | ["http://example.com/foo/bar" "/a/path"] "http://example.com/a/path" 34 | ["http://example.com/foo/bar" "a/relative/path"] "http://example.com/foo/a/relative/path" 35 | ["http://example.com/foo/bar/" "a/relative/path"] "http://example.com/foo/bar/a/relative/path" 36 | ["/foo/bar/" "a/relative/path"] "/foo/bar/a/relative/path" 37 | ["http://example.com" "a/relative/path"] "http://example.com/a/relative/path" 38 | ["http://example.com/a/b/c/d/" "../../x/y"] "http://example.com/a/b/x/y") 39 | 40 | (testing "https://www.w3.org/2004/04/uri-rel-test.html" 41 | (are [x y] (= y (str (uri/join (uri/parse "http://a/b/c/d;p?q") (uri/parse x)))) 42 | "g" "http://a/b/c/g" 43 | "./g" "http://a/b/c/g" 44 | "g/" "http://a/b/c/g/" 45 | "/g" "http://a/g" 46 | "//g" "http://g" 47 | "?y" "http://a/b/c/d;p?y" 48 | "g?y" "http://a/b/c/g?y" 49 | "#s" "http://a/b/c/d;p?q#s" 50 | "g#s" "http://a/b/c/g#s" 51 | "g?y#s" "http://a/b/c/g?y#s" 52 | ";x" "http://a/b/c/;x" 53 | "g;x" "http://a/b/c/g;x" 54 | "g;x?y#s" "http://a/b/c/g;x?y#s" 55 | "" "http://a/b/c/d;p?q" 56 | "." "http://a/b/c/" 57 | "./" "http://a/b/c/" 58 | ".." "http://a/b/" 59 | "../" "http://a/b/" 60 | "../g" "http://a/b/g" 61 | "../.." "http://a/" 62 | "../../" "http://a/" 63 | "../../g" "http://a/g" 64 | "../../../g" "http://a/g" 65 | "../../../../g" "http://a/g" 66 | "/./g" "http://a/g" 67 | "/g" "http://a/g" 68 | "g." "http://a/b/c/g." 69 | ".g" "http://a/b/c/.g" 70 | "g.." "http://a/b/c/g.." 71 | "..g" "http://a/b/c/..g" 72 | "./../g" "http://a/b/g" 73 | "./g/" "http://a/b/c/g/" 74 | "g/h" "http://a/b/c/g/h" 75 | "h" "http://a/b/c/h" 76 | "g;x=1/./y" "http://a/b/c/g;x=1/y" 77 | "g;x=1/../y" "http://a/b/c/y" 78 | "g?y/./x" "http://a/b/c/g?y/./x" 79 | "g?y/../x" "http://a/b/c/g?y/../x" 80 | "g#s/./x" "http://a/b/c/g#s/./x" 81 | "g#s/../x" "http://a/b/c/g#s/../x" 82 | "http:g" "http:g")) 83 | 84 | (testing "coerces its arguments" 85 | (is (= (uri/join "http://x/y/z" "/a/b/c") (uri/parse "http://x/a/b/c"))) 86 | #?(:clj 87 | (is (= (uri/join (java.net.URI. "http://x/y/z") "/a/b/c") (uri/parse "http://x/a/b/c")))))) 88 | 89 | (deftest lambdaisland-uri-URI 90 | (let [example "http://usr:pwd@example.com:8080/path?query=value#fragment" 91 | parsed (uri/uri example)] 92 | (testing "it allows keyword based access" 93 | (is (= (:scheme parsed) "http")) 94 | (is (= (:user parsed) "usr")) 95 | (is (= (:password parsed) "pwd")) 96 | (is (= (:host parsed) "example.com")) 97 | (is (= (:port parsed) "8080")) 98 | (is (= (:path parsed) "/path")) 99 | (is (= (:query parsed) "query=value")) 100 | (is (= (:fragment parsed) "fragment"))) 101 | #?(:bb nil 102 | :default 103 | (testing "it allows map-style access" 104 | (is (= (parsed :scheme) "http")) 105 | (is (= (parsed :user) "usr")) 106 | (is (= (parsed :password) "pwd")) 107 | (is (= (parsed :host) "example.com")) 108 | (is (= (parsed :port) "8080")) 109 | (is (= (parsed :path) "/path")) 110 | (is (= (parsed :query) "query=value")) 111 | (is (= (parsed :fragment) "fragment")))) 112 | (testing "it converts correctly to string" 113 | (is (= (str parsed) example))))) 114 | 115 | (deftest lambdaisland-uri-relative? 116 | (are [x] (uri/relative? (uri/parse x)) 117 | "//example.com" 118 | "/some/path" 119 | "?only=a-query" 120 | "#only-a-fragment" 121 | "//example.com:8080/foo/bar?baz#baq") 122 | (are [x] (uri/absolute? (uri/parse x)) 123 | "http://example.com" 124 | "https://example.com:8080/foo/bar?baz#baq")) 125 | 126 | (deftest query-map-test 127 | (is (= {:foo "bar", :aaa "bbb"} 128 | (uri/query-map "http://example.com?foo=bar&aaa=bbb"))) 129 | 130 | (is (= {"foo" "bar", "aaa" "bbb"} 131 | (uri/query-map "http://example.com?foo=bar&aaa=bbb" {:keywordize? false}))) 132 | 133 | (is (= {:id ["1" "2"]} 134 | (uri/query-map "?id=1&id=2"))) 135 | 136 | (is (= {:id "2"} 137 | (uri/query-map "?id=1&id=2" {:multikeys :never}))) 138 | 139 | (is (= {:foo ["bar"], :id ["2"]} 140 | (uri/query-map "?foo=bar&id=2" {:multikeys :always}))) 141 | 142 | (is (= {:foo " +&xxx=123"} 143 | (uri/query-map "?foo=%20%2B%26xxx%3D123"))) 144 | 145 | (is (= {:foo "aaa=bbb"} 146 | (uri/query-map "?foo=aaa=bbb"))) 147 | 148 | (is (= [:a :b :c :d :e :f :g :h :i] 149 | (keys (uri/query-map "http://example.com?a=1&b=2&c=3&d=4&e=5&f=6&g=7&h=8&i=9" 150 | {:into (sorted-map)})))) 151 | 152 | (is (= nil (uri/query-map "http://example.com/path")))) 153 | 154 | (deftest assoc-query-test 155 | (is (= (uri/uri "http://example.com?foo=baq&aaa=bbb&hello=world") 156 | (uri/assoc-query "http://example.com?foo=bar&aaa=bbb" 157 | :foo "baq" 158 | :hello "world"))) 159 | 160 | (is (= (uri/uri "http://example.com?foo=baq&aaa=bbb&hello=world") 161 | (uri/assoc-query* "http://example.com?foo=bar&aaa=bbb" 162 | {:foo "baq" 163 | :hello "world"}))) 164 | 165 | (is (= (uri/uri "?id=1&id=2") 166 | (uri/assoc-query* "" (uri/query-map "?id=1&id=2")))) 167 | 168 | (is (= (uri/uri "?id=1") 169 | (uri/assoc-query "?id=1&name=jack" :name nil))) 170 | 171 | (is (= (uri/uri "?foo=+%2B%26%3D") 172 | (uri/assoc-query "" :foo " +&="))) 173 | 174 | (is (= "a=a+b&b=b+c" 175 | (-> "/foo" 176 | (uri/assoc-query* {:a "a b"}) 177 | (uri/assoc-query* {:b "b c"}) 178 | :query))) 179 | 180 | (is (= {:a "a b"} 181 | (-> "/foo" 182 | (uri/assoc-query* {:a "a b"}) 183 | uri/query-map)))) 184 | 185 | (deftest query-string->seq-test 186 | (are [query-string expected] 187 | (= expected (uri/query-string->seq query-string)) 188 | nil nil 189 | "" nil 190 | "a=1&b=2&a=3" [["a" "1"] ["b" "2"] ["a" "3"]] 191 | "a=1&b=" [["a" "1"] ["b" ""]])) 192 | 193 | (deftest seq->query-string-test 194 | (are [seq-of-tuples expected] 195 | (= expected (uri/seq->query-string seq-of-tuples)) 196 | nil "" 197 | [] "" 198 | [["a" "1"] ["b" "2"] ["a" "3"]] "a=1&b=2&a=3" 199 | [["a" "1"] ["b" ""]] "a=1&b=")) 200 | 201 | (deftest uri-predicate-test 202 | (is (true? (uri/uri? (uri/uri "/foo"))))) 203 | 204 | (def query-map-gen 205 | (gen/map (gen/such-that #(not= ":/" (str %)) gen/keyword) 206 | gen/string)) 207 | 208 | (tc/defspec query-string-round-trips 100 209 | (prop/for-all [q query-map-gen] 210 | (let [res (-> q 211 | uri/map->query-string 212 | uri/query-string->map)] 213 | (or (and (empty? q) (empty? res)) ;; (= nil {}) 214 | (= q res))))) 215 | 216 | (deftest backslash-in-authority-test 217 | ;; A backslash is not technically a valid character in a URI (see RFC 3986 218 | ;; section 2), and so should always be percent encoded. The problem is that 219 | ;; user-facing software (e.g. browsers) rarely if ever rejects invalid 220 | ;; URIs/URLs, leading to ad-hoc rules about how to map the set of invalid URIs 221 | ;; to valid URIs. All modern browsers now interpret a backslash as a forward 222 | ;; slash, which changes the interpretation of the URI. For this test (and 223 | ;; accompanying patch) we only care about the specific case of a backslash 224 | ;; appearing inside the authority section, since this authority or _origin_ is 225 | ;; regularly used to inform security policies, e.g. to check if code served 226 | ;; from a certain origin has access to resources with the same origin. In this 227 | ;; case we partially mimic what browsers do, by treating the backslash as a 228 | ;; delimiter which starts the path section, even though we don't replace it 229 | ;; with a forward slash, but leave it as-is in the parsed result. 230 | (let [{:keys [host path user]} 231 | (uri/uri "https://example.com\\@gaiwan.co")] 232 | (is (= "example.com" host)) 233 | (is (= nil user)) 234 | (is (= "\\@gaiwan.co" path)))) 235 | -------------------------------------------------------------------------------- /tests.edn: -------------------------------------------------------------------------------- 1 | #kaocha/v1 2 | {:tests [{:id :clj} 3 | {:id :cljs 4 | :type :kaocha.type/cljs}]} 5 | --------------------------------------------------------------------------------