├── .github ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── COPYRIGHT ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── LICENSE-THIRD-PARTY ├── README.md ├── rustfmt.toml ├── src ├── base.rs ├── cmp.rs ├── common │ ├── localize │ │ ├── macos │ │ │ ├── fruity.rs │ │ │ └── mod.rs │ │ └── mod.rs │ └── mod.rs ├── error.rs ├── lib.rs └── windows │ ├── localize.rs │ └── mod.rs └── tests ├── common.rs ├── edge_cases.rs ├── integration.rs ├── localization.rs ├── rust.rs └── windows.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | schedule: 9 | - cron: 0 0 * * FRI 10 | 11 | jobs: 12 | build: 13 | runs-on: ${{ matrix.platform }} 14 | steps: 15 | - uses: dylni/build-actions/build@master 16 | timeout-minutes: 10 17 | strategy: 18 | matrix: 19 | platform: [ubuntu-latest, windows-latest] 20 | test: 21 | needs: [build] 22 | runs-on: ${{ matrix.platform }} 23 | steps: 24 | - uses: dylni/build-actions/test@master 25 | with: 26 | version: ${{ matrix.version }} 27 | - run: cargo test --features localization 28 | timeout-minutes: 10 29 | strategy: 30 | matrix: 31 | platform: [macos-latest, ubuntu-latest, windows-latest] 32 | version: [1.80.0, stable, beta, nightly] 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 dylni (https://github.com/dylni) 2 | 3 | Some files also include explicit copyright notices. 4 | 5 | Licensed under the Apache License, Version 2.0 or the MIT 6 | license , at your option. All files in this project may not be 7 | copied, modified, or distributed except according to those terms. 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "normpath" 3 | version = "1.3.0" 4 | authors = ["dylni"] 5 | edition = "2021" 6 | rust-version = "1.80.0" 7 | description = """ 8 | More reliable path manipulation 9 | """ 10 | readme = "README.md" 11 | repository = "https://github.com/dylni/normpath" 12 | license = "MIT OR Apache-2.0" 13 | keywords = ["absolute", "canonicalize", "path", "normalize", "windows"] 14 | categories = ["command-line-interface", "filesystem", "os"] 15 | exclude = [".*", "tests.rs", "/rustfmt.toml", "/src/bin", "/tests"] 16 | 17 | [package.metadata.docs.rs] 18 | all-features = true 19 | rustc-args = ["--cfg", "normpath_docs_rs"] 20 | rustdoc-args = ["--cfg", "normpath_docs_rs"] 21 | 22 | [dependencies] 23 | print_bytes = { version = "2.0", features = ["os_str_bytes"], optional = true } 24 | serde = { version = "1.0", optional = true } 25 | uniquote = { version = "4.0", optional = true } 26 | 27 | [target.'cfg(windows)'.dependencies] 28 | windows-sys = { version = "0.59", features = ["Win32_Storage_FileSystem"] } 29 | 30 | [dev-dependencies] 31 | bincode = "1.0" 32 | tempfile = "3.0" 33 | 34 | [target.'cfg(windows)'.dev-dependencies] 35 | windows-sys = { version = "0.59", features = ["Win32_Foundation"] } 36 | 37 | [target.'cfg(not(windows))'.dev-dependencies] 38 | libc = "0.2" 39 | 40 | [features] 41 | localization = ["windows-sys/Win32_UI_Shell", "windows-sys/Win32_UI_WindowsAndMessaging"] 42 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 dylni (https://github.com/dylni) 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 | -------------------------------------------------------------------------------- /LICENSE-THIRD-PARTY: -------------------------------------------------------------------------------- 1 | =============================================================================== 2 | 3 | The Rust Programming Language 4 | https://github.com/rust-lang/rust/blob/b1277d04db0dc8009037e872a1be7cdc2bd74a43/LICENSE-MIT 5 | 6 | Permission is hereby granted, free of charge, to any 7 | person obtaining a copy of this software and associated 8 | documentation files (the "Software"), to deal in the 9 | Software without restriction, including without 10 | limitation the rights to use, copy, modify, merge, 11 | publish, distribute, sublicense, and/or sell copies of 12 | the Software, and to permit persons to whom the Software 13 | is furnished to do so, subject to the following 14 | conditions: 15 | 16 | The above copyright notice and this permission notice 17 | shall be included in all copies or substantial portions 18 | of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 21 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 22 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 23 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 24 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 25 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 26 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 27 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 28 | DEALINGS IN THE SOFTWARE. 29 | 30 | =============================================================================== 31 | 32 | The Rust Programming Language 33 | https://github.com/rust-lang/rust/blob/b1277d04db0dc8009037e872a1be7cdc2bd74a43/LICENSE-APACHE 34 | 35 | Apache License 36 | Version 2.0, January 2004 37 | http://www.apache.org/licenses/ 38 | 39 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 40 | 41 | 1. Definitions. 42 | 43 | "License" shall mean the terms and conditions for use, reproduction, 44 | and distribution as defined by Sections 1 through 9 of this document. 45 | 46 | "Licensor" shall mean the copyright owner or entity authorized by 47 | the copyright owner that is granting the License. 48 | 49 | "Legal Entity" shall mean the union of the acting entity and all 50 | other entities that control, are controlled by, or are under common 51 | control with that entity. For the purposes of this definition, 52 | "control" means (i) the power, direct or indirect, to cause the 53 | direction or management of such entity, whether by contract or 54 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 55 | outstanding shares, or (iii) beneficial ownership of such entity. 56 | 57 | "You" (or "Your") shall mean an individual or Legal Entity 58 | exercising permissions granted by this License. 59 | 60 | "Source" form shall mean the preferred form for making modifications, 61 | including but not limited to software source code, documentation 62 | source, and configuration files. 63 | 64 | "Object" form shall mean any form resulting from mechanical 65 | transformation or translation of a Source form, including but 66 | not limited to compiled object code, generated documentation, 67 | and conversions to other media types. 68 | 69 | "Work" shall mean the work of authorship, whether in Source or 70 | Object form, made available under the License, as indicated by a 71 | copyright notice that is included in or attached to the work 72 | (an example is provided in the Appendix below). 73 | 74 | "Derivative Works" shall mean any work, whether in Source or Object 75 | form, that is based on (or derived from) the Work and for which the 76 | editorial revisions, annotations, elaborations, or other modifications 77 | represent, as a whole, an original work of authorship. For the purposes 78 | of this License, Derivative Works shall not include works that remain 79 | separable from, or merely link (or bind by name) to the interfaces of, 80 | the Work and Derivative Works thereof. 81 | 82 | "Contribution" shall mean any work of authorship, including 83 | the original version of the Work and any modifications or additions 84 | to that Work or Derivative Works thereof, that is intentionally 85 | submitted to Licensor for inclusion in the Work by the copyright owner 86 | or by an individual or Legal Entity authorized to submit on behalf of 87 | the copyright owner. For the purposes of this definition, "submitted" 88 | means any form of electronic, verbal, or written communication sent 89 | to the Licensor or its representatives, including but not limited to 90 | communication on electronic mailing lists, source code control systems, 91 | and issue tracking systems that are managed by, or on behalf of, the 92 | Licensor for the purpose of discussing and improving the Work, but 93 | excluding communication that is conspicuously marked or otherwise 94 | designated in writing by the copyright owner as "Not a Contribution." 95 | 96 | "Contributor" shall mean Licensor and any individual or Legal Entity 97 | on behalf of whom a Contribution has been received by Licensor and 98 | subsequently incorporated within the Work. 99 | 100 | 2. Grant of Copyright License. Subject to the terms and conditions of 101 | this License, each Contributor hereby grants to You a perpetual, 102 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 103 | copyright license to reproduce, prepare Derivative Works of, 104 | publicly display, publicly perform, sublicense, and distribute the 105 | Work and such Derivative Works in Source or Object form. 106 | 107 | 3. Grant of Patent License. Subject to the terms and conditions of 108 | this License, each Contributor hereby grants to You a perpetual, 109 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 110 | (except as stated in this section) patent license to make, have made, 111 | use, offer to sell, sell, import, and otherwise transfer the Work, 112 | where such license applies only to those patent claims licensable 113 | by such Contributor that are necessarily infringed by their 114 | Contribution(s) alone or by combination of their Contribution(s) 115 | with the Work to which such Contribution(s) was submitted. If You 116 | institute patent litigation against any entity (including a 117 | cross-claim or counterclaim in a lawsuit) alleging that the Work 118 | or a Contribution incorporated within the Work constitutes direct 119 | or contributory patent infringement, then any patent licenses 120 | granted to You under this License for that Work shall terminate 121 | as of the date such litigation is filed. 122 | 123 | 4. Redistribution. You may reproduce and distribute copies of the 124 | Work or Derivative Works thereof in any medium, with or without 125 | modifications, and in Source or Object form, provided that You 126 | meet the following conditions: 127 | 128 | (a) You must give any other recipients of the Work or 129 | Derivative Works a copy of this License; and 130 | 131 | (b) You must cause any modified files to carry prominent notices 132 | stating that You changed the files; and 133 | 134 | (c) You must retain, in the Source form of any Derivative Works 135 | that You distribute, all copyright, patent, trademark, and 136 | attribution notices from the Source form of the Work, 137 | excluding those notices that do not pertain to any part of 138 | the Derivative Works; and 139 | 140 | (d) If the Work includes a "NOTICE" text file as part of its 141 | distribution, then any Derivative Works that You distribute must 142 | include a readable copy of the attribution notices contained 143 | within such NOTICE file, excluding those notices that do not 144 | pertain to any part of the Derivative Works, in at least one 145 | of the following places: within a NOTICE text file distributed 146 | as part of the Derivative Works; within the Source form or 147 | documentation, if provided along with the Derivative Works; or, 148 | within a display generated by the Derivative Works, if and 149 | wherever such third-party notices normally appear. The contents 150 | of the NOTICE file are for informational purposes only and 151 | do not modify the License. You may add Your own attribution 152 | notices within Derivative Works that You distribute, alongside 153 | or as an addendum to the NOTICE text from the Work, provided 154 | that such additional attribution notices cannot be construed 155 | as modifying the License. 156 | 157 | You may add Your own copyright statement to Your modifications and 158 | may provide additional or different license terms and conditions 159 | for use, reproduction, or distribution of Your modifications, or 160 | for any such Derivative Works as a whole, provided Your use, 161 | reproduction, and distribution of the Work otherwise complies with 162 | the conditions stated in this License. 163 | 164 | 5. Submission of Contributions. Unless You explicitly state otherwise, 165 | any Contribution intentionally submitted for inclusion in the Work 166 | by You to the Licensor shall be under the terms and conditions of 167 | this License, without any additional terms or conditions. 168 | Notwithstanding the above, nothing herein shall supersede or modify 169 | the terms of any separate license agreement you may have executed 170 | with Licensor regarding such Contributions. 171 | 172 | 6. Trademarks. This License does not grant permission to use the trade 173 | names, trademarks, service marks, or product names of the Licensor, 174 | except as required for reasonable and customary use in describing the 175 | origin of the Work and reproducing the content of the NOTICE file. 176 | 177 | 7. Disclaimer of Warranty. Unless required by applicable law or 178 | agreed to in writing, Licensor provides the Work (and each 179 | Contributor provides its Contributions) on an "AS IS" BASIS, 180 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 181 | implied, including, without limitation, any warranties or conditions 182 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 183 | PARTICULAR PURPOSE. You are solely responsible for determining the 184 | appropriateness of using or redistributing the Work and assume any 185 | risks associated with Your exercise of permissions under this License. 186 | 187 | 8. Limitation of Liability. In no event and under no legal theory, 188 | whether in tort (including negligence), contract, or otherwise, 189 | unless required by applicable law (such as deliberate and grossly 190 | negligent acts) or agreed to in writing, shall any Contributor be 191 | liable to You for damages, including any direct, indirect, special, 192 | incidental, or consequential damages of any character arising as a 193 | result of this License or out of the use or inability to use the 194 | Work (including but not limited to damages for loss of goodwill, 195 | work stoppage, computer failure or malfunction, or any and all 196 | other commercial damages or losses), even if such Contributor 197 | has been advised of the possibility of such damages. 198 | 199 | 9. Accepting Warranty or Additional Liability. While redistributing 200 | the Work or Derivative Works thereof, You may choose to offer, 201 | and charge a fee for, acceptance of support, warranty, indemnity, 202 | or other liability obligations and/or rights consistent with this 203 | License. However, in accepting such obligations, You may act only 204 | on Your own behalf and on Your sole responsibility, not on behalf 205 | of any other Contributor, and only if You agree to indemnify, 206 | defend, and hold each Contributor harmless for any liability 207 | incurred by, or claims asserted against, such Contributor by reason 208 | of your accepting any such warranty or additional liability. 209 | 210 | END OF TERMS AND CONDITIONS 211 | 212 | =============================================================================== 213 | 214 | Fruity 215 | https://github.com/nvzqz/fruity/blob/320efcf715c2c5fbd2f3084f671f2be2e03a6f2b/LICENSE-MIT 216 | 217 | MIT License 218 | 219 | Copyright (c) 2020 Nikolai Vazquez 220 | 221 | Permission is hereby granted, free of charge, to any person obtaining a copy 222 | of this software and associated documentation files (the "Software"), to deal 223 | in the Software without restriction, including without limitation the rights 224 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 225 | copies of the Software, and to permit persons to whom the Software is 226 | furnished to do so, subject to the following conditions: 227 | 228 | The above copyright notice and this permission notice shall be included in all 229 | copies or substantial portions of the Software. 230 | 231 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 232 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 233 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 234 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 235 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 236 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 237 | SOFTWARE. 238 | 239 | =============================================================================== 240 | 241 | Fruity 242 | https://github.com/nvzqz/fruity/blob/320efcf715c2c5fbd2f3084f671f2be2e03a6f2b/LICENSE-APACHE 243 | 244 | Apache License 245 | Version 2.0, January 2004 246 | http://www.apache.org/licenses/ 247 | 248 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 249 | 250 | 1. Definitions. 251 | 252 | "License" shall mean the terms and conditions for use, reproduction, 253 | and distribution as defined by Sections 1 through 9 of this document. 254 | 255 | "Licensor" shall mean the copyright owner or entity authorized by 256 | the copyright owner that is granting the License. 257 | 258 | "Legal Entity" shall mean the union of the acting entity and all 259 | other entities that control, are controlled by, or are under common 260 | control with that entity. For the purposes of this definition, 261 | "control" means (i) the power, direct or indirect, to cause the 262 | direction or management of such entity, whether by contract or 263 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 264 | outstanding shares, or (iii) beneficial ownership of such entity. 265 | 266 | "You" (or "Your") shall mean an individual or Legal Entity 267 | exercising permissions granted by this License. 268 | 269 | "Source" form shall mean the preferred form for making modifications, 270 | including but not limited to software source code, documentation 271 | source, and configuration files. 272 | 273 | "Object" form shall mean any form resulting from mechanical 274 | transformation or translation of a Source form, including but 275 | not limited to compiled object code, generated documentation, 276 | and conversions to other media types. 277 | 278 | "Work" shall mean the work of authorship, whether in Source or 279 | Object form, made available under the License, as indicated by a 280 | copyright notice that is included in or attached to the work 281 | (an example is provided in the Appendix below). 282 | 283 | "Derivative Works" shall mean any work, whether in Source or Object 284 | form, that is based on (or derived from) the Work and for which the 285 | editorial revisions, annotations, elaborations, or other modifications 286 | represent, as a whole, an original work of authorship. For the purposes 287 | of this License, Derivative Works shall not include works that remain 288 | separable from, or merely link (or bind by name) to the interfaces of, 289 | the Work and Derivative Works thereof. 290 | 291 | "Contribution" shall mean any work of authorship, including 292 | the original version of the Work and any modifications or additions 293 | to that Work or Derivative Works thereof, that is intentionally 294 | submitted to Licensor for inclusion in the Work by the copyright owner 295 | or by an individual or Legal Entity authorized to submit on behalf of 296 | the copyright owner. For the purposes of this definition, "submitted" 297 | means any form of electronic, verbal, or written communication sent 298 | to the Licensor or its representatives, including but not limited to 299 | communication on electronic mailing lists, source code control systems, 300 | and issue tracking systems that are managed by, or on behalf of, the 301 | Licensor for the purpose of discussing and improving the Work, but 302 | excluding communication that is conspicuously marked or otherwise 303 | designated in writing by the copyright owner as "Not a Contribution." 304 | 305 | "Contributor" shall mean Licensor and any individual or Legal Entity 306 | on behalf of whom a Contribution has been received by Licensor and 307 | subsequently incorporated within the Work. 308 | 309 | 2. Grant of Copyright License. Subject to the terms and conditions of 310 | this License, each Contributor hereby grants to You a perpetual, 311 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 312 | copyright license to reproduce, prepare Derivative Works of, 313 | publicly display, publicly perform, sublicense, and distribute the 314 | Work and such Derivative Works in Source or Object form. 315 | 316 | 3. Grant of Patent License. Subject to the terms and conditions of 317 | this License, each Contributor hereby grants to You a perpetual, 318 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 319 | (except as stated in this section) patent license to make, have made, 320 | use, offer to sell, sell, import, and otherwise transfer the Work, 321 | where such license applies only to those patent claims licensable 322 | by such Contributor that are necessarily infringed by their 323 | Contribution(s) alone or by combination of their Contribution(s) 324 | with the Work to which such Contribution(s) was submitted. If You 325 | institute patent litigation against any entity (including a 326 | cross-claim or counterclaim in a lawsuit) alleging that the Work 327 | or a Contribution incorporated within the Work constitutes direct 328 | or contributory patent infringement, then any patent licenses 329 | granted to You under this License for that Work shall terminate 330 | as of the date such litigation is filed. 331 | 332 | 4. Redistribution. You may reproduce and distribute copies of the 333 | Work or Derivative Works thereof in any medium, with or without 334 | modifications, and in Source or Object form, provided that You 335 | meet the following conditions: 336 | 337 | (a) You must give any other recipients of the Work or 338 | Derivative Works a copy of this License; and 339 | 340 | (b) You must cause any modified files to carry prominent notices 341 | stating that You changed the files; and 342 | 343 | (c) You must retain, in the Source form of any Derivative Works 344 | that You distribute, all copyright, patent, trademark, and 345 | attribution notices from the Source form of the Work, 346 | excluding those notices that do not pertain to any part of 347 | the Derivative Works; and 348 | 349 | (d) If the Work includes a "NOTICE" text file as part of its 350 | distribution, then any Derivative Works that You distribute must 351 | include a readable copy of the attribution notices contained 352 | within such NOTICE file, excluding those notices that do not 353 | pertain to any part of the Derivative Works, in at least one 354 | of the following places: within a NOTICE text file distributed 355 | as part of the Derivative Works; within the Source form or 356 | documentation, if provided along with the Derivative Works; or, 357 | within a display generated by the Derivative Works, if and 358 | wherever such third-party notices normally appear. The contents 359 | of the NOTICE file are for informational purposes only and 360 | do not modify the License. You may add Your own attribution 361 | notices within Derivative Works that You distribute, alongside 362 | or as an addendum to the NOTICE text from the Work, provided 363 | that such additional attribution notices cannot be construed 364 | as modifying the License. 365 | 366 | You may add Your own copyright statement to Your modifications and 367 | may provide additional or different license terms and conditions 368 | for use, reproduction, or distribution of Your modifications, or 369 | for any such Derivative Works as a whole, provided Your use, 370 | reproduction, and distribution of the Work otherwise complies with 371 | the conditions stated in this License. 372 | 373 | 5. Submission of Contributions. Unless You explicitly state otherwise, 374 | any Contribution intentionally submitted for inclusion in the Work 375 | by You to the Licensor shall be under the terms and conditions of 376 | this License, without any additional terms or conditions. 377 | Notwithstanding the above, nothing herein shall supersede or modify 378 | the terms of any separate license agreement you may have executed 379 | with Licensor regarding such Contributions. 380 | 381 | 6. Trademarks. This License does not grant permission to use the trade 382 | names, trademarks, service marks, or product names of the Licensor, 383 | except as required for reasonable and customary use in describing the 384 | origin of the Work and reproducing the content of the NOTICE file. 385 | 386 | 7. Disclaimer of Warranty. Unless required by applicable law or 387 | agreed to in writing, Licensor provides the Work (and each 388 | Contributor provides its Contributions) on an "AS IS" BASIS, 389 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 390 | implied, including, without limitation, any warranties or conditions 391 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 392 | PARTICULAR PURPOSE. You are solely responsible for determining the 393 | appropriateness of using or redistributing the Work and assume any 394 | risks associated with Your exercise of permissions under this License. 395 | 396 | 8. Limitation of Liability. In no event and under no legal theory, 397 | whether in tort (including negligence), contract, or otherwise, 398 | unless required by applicable law (such as deliberate and grossly 399 | negligent acts) or agreed to in writing, shall any Contributor be 400 | liable to You for damages, including any direct, indirect, special, 401 | incidental, or consequential damages of any character arising as a 402 | result of this License or out of the use or inability to use the 403 | Work (including but not limited to damages for loss of goodwill, 404 | work stoppage, computer failure or malfunction, or any and all 405 | other commercial damages or losses), even if such Contributor 406 | has been advised of the possibility of such damages. 407 | 408 | 9. Accepting Warranty or Additional Liability. While redistributing 409 | the Work or Derivative Works thereof, You may choose to offer, 410 | and charge a fee for, acceptance of support, warranty, indemnity, 411 | or other liability obligations and/or rights consistent with this 412 | License. However, in accepting such obligations, You may act only 413 | on Your own behalf and on Your sole responsibility, not on behalf 414 | of any other Contributor, and only if You agree to indemnify, 415 | defend, and hold each Contributor harmless for any liability 416 | incurred by, or claims asserted against, such Contributor by reason 417 | of your accepting any such warranty or additional liability. 418 | 419 | END OF TERMS AND CONDITIONS 420 | 421 | APPENDIX: How to apply the Apache License to your work. 422 | 423 | To apply the Apache License to your work, attach the following 424 | boilerplate notice, with the fields enclosed by brackets "[]" 425 | replaced with your own identifying information. (Don't include 426 | the brackets!) The text should be enclosed in the appropriate 427 | comment syntax for the file format. We also recommend that a 428 | file or class name and description of purpose be included on the 429 | same "printed page" as the copyright notice for easier 430 | identification within third-party archives. 431 | 432 | Copyright [yyyy] [name of copyright owner] 433 | 434 | Licensed under the Apache License, Version 2.0 (the "License"); 435 | you may not use this file except in compliance with the License. 436 | You may obtain a copy of the License at 437 | 438 | http://www.apache.org/licenses/LICENSE-2.0 439 | 440 | Unless required by applicable law or agreed to in writing, software 441 | distributed under the License is distributed on an "AS IS" BASIS, 442 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 443 | See the License for the specific language governing permissions and 444 | limitations under the License. 445 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NormPath 2 | 3 | This crate provides methods to normalize paths in the recommended way for the 4 | operating system. 5 | 6 | It was made to fix a recurring bug caused by using [`fs::canonicalize`] on 7 | Windows: [#45067], [#48249], [#52440], [#55812], [#58613], [#59107], [#74327]. 8 | Normalization is usually a better choice unless you specifically need a 9 | canonical path. 10 | 11 | [![GitHub Build Status](https://github.com/dylni/normpath/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/dylni/normpath/actions/workflows/build.yml?query=branch%3Amaster) 12 | 13 | ## Usage 14 | 15 | Add the following lines to your "Cargo.toml" file: 16 | 17 | ```toml 18 | [dependencies] 19 | normpath = "1.3" 20 | ``` 21 | 22 | See the [documentation] for available functionality and examples. 23 | 24 | ## Rust version support 25 | 26 | The minimum supported Rust toolchain version is currently Rust 1.80.0. 27 | 28 | Minor version updates may increase this version requirement. However, the 29 | previous two Rust releases will always be supported. If the minimum Rust 30 | version must not be increased, use a tilde requirement to prevent updating this 31 | crate's minor version: 32 | 33 | ```toml 34 | [dependencies] 35 | normpath = "~1.3" 36 | ``` 37 | 38 | ## License 39 | 40 | Licensing terms are specified in [COPYRIGHT]. 41 | 42 | Unless you explicitly state otherwise, any contribution submitted for inclusion 43 | in this crate, as defined in [LICENSE-APACHE], shall be licensed according to 44 | [COPYRIGHT], without any additional terms or conditions. 45 | 46 | ### Third-party content 47 | 48 | This crate includes copies and modifications of content developed by third 49 | parties: 50 | 51 | - [src/cmp.rs] and [tests/rust.rs] contain modifications of code from The Rust 52 | Programming Language, licensed under the MIT License or the Apache License, 53 | Version 2.0. 54 | 55 | - [src/common/localize/macos/fruity.rs] contains modifications of code from 56 | crate [fruity], licensed under the MIT License or the Apache License, 57 | Version 2.0. 58 | 59 | See those files for more details. 60 | 61 | Copies of third-party licenses can be found in [LICENSE-THIRD-PARTY]. 62 | 63 | [#45067]: https://github.com/rust-lang/rust/issues/45067 64 | [#48249]: https://github.com/rust-lang/rust/issues/48249 65 | [#52440]: https://github.com/rust-lang/rust/issues/52440 66 | [#55812]: https://github.com/rust-lang/rust/issues/55812 67 | [#58613]: https://github.com/rust-lang/rust/issues/58613 68 | [#59107]: https://github.com/rust-lang/rust/issues/59107 69 | [#74327]: https://github.com/rust-lang/rust/issues/74327 70 | [COPYRIGHT]: https://github.com/dylni/normpath/blob/master/COPYRIGHT 71 | [documentation]: https://docs.rs/normpath 72 | [fruity]: https://crates.io/crates/fruity 73 | [`fs::canonicalize`]: https://doc.rust-lang.org/std/fs/fn.canonicalize.html 74 | [LICENSE-APACHE]: https://github.com/dylni/normpath/blob/master/LICENSE-APACHE 75 | [LICENSE-THIRD-PARTY]: https://github.com/dylni/normpath/blob/master/LICENSE-THIRD-PARTY 76 | [src/cmp.rs]: https://github.com/dylni/normpath/blob/master/src/cmp.rs 77 | [src/common/localize/macos/fruity.rs]: https://github.com/dylni/normpath/blob/master/src/common/localize/macos/fruity.rs 78 | [tests/rust.rs]: https://github.com/dylni/normpath/blob/master/tests/rust.rs 79 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 79 2 | -------------------------------------------------------------------------------- /src/base.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Borrow; 2 | use std::borrow::Cow; 3 | use std::cmp::Ordering; 4 | use std::ffi::OsStr; 5 | use std::ffi::OsString; 6 | use std::fs::Metadata; 7 | use std::fs::ReadDir; 8 | use std::hash::Hash; 9 | use std::hash::Hasher; 10 | use std::io; 11 | use std::mem; 12 | use std::ops::Deref; 13 | use std::path::Component; 14 | use std::path::Components; 15 | use std::path::Path; 16 | use std::path::PathBuf; 17 | 18 | use super::error::MissingPrefixBufError; 19 | use super::error::MissingPrefixError; 20 | use super::error::ParentError; 21 | use super::imp; 22 | use super::PathExt; 23 | 24 | fn cow_path_into_base_path(path: Cow<'_, Path>) -> Cow<'_, BasePath> { 25 | debug_assert!(imp::is_base(&path)); 26 | 27 | match path { 28 | Cow::Borrowed(path) => { 29 | Cow::Borrowed(BasePath::from_inner(path.as_os_str())) 30 | } 31 | Cow::Owned(path) => Cow::Owned(BasePathBuf(path)), 32 | } 33 | } 34 | 35 | /// A borrowed path that has a [prefix] on Windows. 36 | /// 37 | /// Note that comparison traits such as [`PartialEq`] will compare paths 38 | /// literally instead of comparing components. The former is more efficient and 39 | /// easier to use correctly. 40 | /// 41 | /// # Safety 42 | /// 43 | /// This type should not be used for memory safety, but implementations can 44 | /// panic if this path is missing a prefix on Windows. A safe `new_unchecked` 45 | /// method might be added later that can safely create invalid base paths. 46 | /// 47 | /// [prefix]: ::std::path::Prefix 48 | #[derive(Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 49 | #[repr(transparent)] 50 | pub struct BasePath(pub(super) OsStr); 51 | 52 | impl BasePath { 53 | pub(super) fn from_inner(path: &OsStr) -> &Self { 54 | // SAFETY: This struct has a layout that makes this operation safe. 55 | unsafe { mem::transmute(path) } 56 | } 57 | 58 | /// Creates a new base path. 59 | /// 60 | /// On Windows, if `path` is missing a [prefix], it will be joined to the 61 | /// current directory. 62 | /// 63 | /// # Errors 64 | /// 65 | /// Returns an error if reading the current directory fails. 66 | /// 67 | /// # Examples 68 | /// 69 | /// ``` 70 | /// # use std::io; 71 | /// use std::path::Path; 72 | /// 73 | /// use normpath::BasePath; 74 | /// 75 | /// if cfg!(windows) { 76 | /// let path = Path::new(r"X:\foo\bar"); 77 | /// assert_eq!(path, *BasePath::new(path)?); 78 | /// 79 | /// assert!(BasePath::new(Path::new(r"foo\bar")).is_ok()); 80 | /// } 81 | /// # 82 | /// # Ok::<_, io::Error>(()) 83 | /// ``` 84 | /// 85 | /// [prefix]: ::std::path::Prefix 86 | #[inline] 87 | pub fn new<'a, P>(path: P) -> io::Result> 88 | where 89 | P: Into>, 90 | { 91 | let path = path.into(); 92 | match path { 93 | Cow::Borrowed(path) => Self::try_new(path) 94 | .map(Cow::Borrowed) 95 | .or_else(|_| imp::to_base(path).map(Cow::Owned)), 96 | Cow::Owned(path) => BasePathBuf::new(path).map(Cow::Owned), 97 | } 98 | } 99 | 100 | /// Creates a new base path. 101 | /// 102 | /// # Errors 103 | /// 104 | /// On Windows, returns an error if `path` is missing a [prefix]. 105 | /// 106 | /// # Examples 107 | /// 108 | /// ``` 109 | /// use std::path::Path; 110 | /// 111 | /// # use normpath::error::MissingPrefixError; 112 | /// use normpath::BasePath; 113 | /// 114 | /// if cfg!(windows) { 115 | /// let path = r"X:\foo\bar"; 116 | /// assert_eq!(Path::new(path), BasePath::try_new(path)?); 117 | /// 118 | /// assert!(BasePath::try_new(r"foo\bar").is_err()); 119 | /// } 120 | /// # 121 | /// # Ok::<_, MissingPrefixError>(()) 122 | /// ``` 123 | /// 124 | /// [prefix]: ::std::path::Prefix 125 | #[inline] 126 | pub fn try_new

(path: &P) -> Result<&Self, MissingPrefixError> 127 | where 128 | P: AsRef + ?Sized, 129 | { 130 | let path = path.as_ref(); 131 | if imp::is_base(path) { 132 | Ok(Self::from_inner(path.as_os_str())) 133 | } else { 134 | Err(MissingPrefixError(())) 135 | } 136 | } 137 | 138 | /// Returns a reference to the wrapped path as a platform string. 139 | #[inline] 140 | #[must_use] 141 | pub fn as_os_str(&self) -> &OsStr { 142 | &self.0 143 | } 144 | 145 | /// Returns a reference to the wrapped path. 146 | #[inline] 147 | #[must_use] 148 | pub fn as_path(&self) -> &Path { 149 | Path::new(&self.0) 150 | } 151 | 152 | /// Equivalent to [`Path::canonicalize`]. 153 | #[inline] 154 | pub fn canonicalize(&self) -> io::Result { 155 | self.as_path().canonicalize().map(|base| { 156 | debug_assert!(imp::is_base(&base)); 157 | BasePathBuf(base) 158 | }) 159 | } 160 | 161 | /// Equivalent to [`Path::components`]. 162 | #[inline] 163 | pub fn components(&self) -> Components<'_> { 164 | self.as_path().components() 165 | } 166 | 167 | /// Equivalent to [`Path::ends_with`]. 168 | #[inline] 169 | #[must_use] 170 | pub fn ends_with

(&self, child: P) -> bool 171 | where 172 | P: AsRef, 173 | { 174 | self.as_path().ends_with(child) 175 | } 176 | 177 | /// Equivalent to [`Path::exists`]. 178 | #[inline] 179 | #[must_use] 180 | pub fn exists(&self) -> bool { 181 | self.as_path().exists() 182 | } 183 | 184 | /// Equivalent to [`PathExt::expand`]. 185 | #[inline] 186 | pub fn expand(&self) -> io::Result> { 187 | self.as_path().expand().map(cow_path_into_base_path) 188 | } 189 | 190 | /// Equivalent to [`Path::extension`]. 191 | #[inline] 192 | #[must_use] 193 | pub fn extension(&self) -> Option<&OsStr> { 194 | self.as_path().extension() 195 | } 196 | 197 | /// Equivalent to [`Path::file_name`]. 198 | #[inline] 199 | #[must_use] 200 | pub fn file_name(&self) -> Option<&OsStr> { 201 | self.as_path().file_name() 202 | } 203 | 204 | /// Equivalent to [`Path::file_stem`]. 205 | #[inline] 206 | #[must_use] 207 | pub fn file_stem(&self) -> Option<&OsStr> { 208 | self.as_path().file_stem() 209 | } 210 | 211 | /// Equivalent to [`Path::has_root`]. 212 | #[inline] 213 | #[must_use] 214 | pub fn has_root(&self) -> bool { 215 | self.as_path().has_root() 216 | } 217 | 218 | /// Equivalent to [`Path::is_absolute`]. 219 | #[inline] 220 | #[must_use] 221 | pub fn is_absolute(&self) -> bool { 222 | self.as_path().is_absolute() 223 | } 224 | 225 | /// Equivalent to [`Path::is_dir`]. 226 | #[inline] 227 | #[must_use] 228 | pub fn is_dir(&self) -> bool { 229 | self.as_path().is_dir() 230 | } 231 | 232 | /// Equivalent to [`Path::is_file`]. 233 | #[inline] 234 | #[must_use] 235 | pub fn is_file(&self) -> bool { 236 | self.as_path().is_file() 237 | } 238 | 239 | /// Equivalent to [`Path::is_relative`]. 240 | #[inline] 241 | #[must_use] 242 | pub fn is_relative(&self) -> bool { 243 | self.as_path().is_relative() 244 | } 245 | 246 | /// Equivalent to [`Path::is_symlink`]. 247 | #[inline] 248 | #[must_use] 249 | pub fn is_symlink(&self) -> bool { 250 | self.as_path().is_symlink() 251 | } 252 | 253 | /// An improved version of [`Path::join`] that handles more edge cases. 254 | /// 255 | /// For example, on Windows, leading `.` and `..` components of `path` will 256 | /// be normalized if possible. If `self` is a [verbatim] path, it would be 257 | /// invalid to normalize them later. 258 | /// 259 | /// You should still call [`normalize`] before [`parent`] to normalize some 260 | /// additional components. 261 | /// 262 | /// # Examples 263 | /// 264 | /// ``` 265 | /// use std::path::Path; 266 | /// 267 | /// use normpath::BasePath; 268 | /// 269 | /// if cfg!(windows) { 270 | /// assert_eq!( 271 | /// Path::new(r"\\?\foo\baz\test.rs"), 272 | /// BasePath::try_new(r"\\?\foo\bar") 273 | /// .unwrap() 274 | /// .join("../baz/test.rs"), 275 | /// ); 276 | /// } 277 | /// ``` 278 | /// 279 | /// [`normalize`]: Self::normalize 280 | /// [`parent`]: Self::parent 281 | /// [verbatim]: ::std::path::Prefix::is_verbatim 282 | #[inline] 283 | pub fn join

(&self, path: P) -> BasePathBuf 284 | where 285 | P: AsRef, 286 | { 287 | let mut base = self.to_owned(); 288 | base.push(path); 289 | base 290 | } 291 | 292 | /// Equivalent to [`PathExt::localize_name`]. 293 | #[cfg(feature = "localization")] 294 | #[cfg_attr(normpath_docs_rs, doc(cfg(feature = "localization")))] 295 | #[inline] 296 | #[must_use] 297 | pub fn localize_name(&self) -> Cow<'_, OsStr> { 298 | self.as_path().localize_name() 299 | } 300 | 301 | /// Equivalent to [`Path::metadata`]. 302 | #[inline] 303 | pub fn metadata(&self) -> io::Result { 304 | self.as_path().metadata() 305 | } 306 | 307 | /// Equivalent to [`PathExt::normalize`]. 308 | #[inline] 309 | pub fn normalize(&self) -> io::Result { 310 | self.as_path().normalize() 311 | } 312 | 313 | /// Equivalent to [`PathExt::normalize_virtually`]. 314 | #[cfg(any(doc, windows))] 315 | #[cfg_attr(normpath_docs_rs, doc(cfg(windows)))] 316 | #[inline] 317 | pub fn normalize_virtually(&self) -> io::Result { 318 | self.as_path().normalize_virtually() 319 | } 320 | 321 | fn check_parent(&self) -> Result<(), ParentError> { 322 | self.components() 323 | .next_back() 324 | .filter(|x| matches!(x, Component::Normal(_) | Component::RootDir)) 325 | .map(|_| ()) 326 | .ok_or(ParentError(())) 327 | } 328 | 329 | /// Returns this path without its last component. 330 | /// 331 | /// Returns `Ok(None)` if the last component is [`Component::RootDir`]. 332 | /// 333 | /// You should usually only call this method on [normalized] paths. They 334 | /// will prevent an unexpected path from being returned due to symlinks, 335 | /// and some `.` and `..` components will be normalized. 336 | /// 337 | /// # Errors 338 | /// 339 | /// Returns an error if the last component is not [`Component::Normal`] or 340 | /// [`Component::RootDir`]. To ignore this error, use [`parent_unchecked`]. 341 | /// 342 | /// # Examples 343 | /// 344 | /// ``` 345 | /// use std::path::Path; 346 | /// 347 | /// # use normpath::error::ParentError; 348 | /// use normpath::BasePath; 349 | /// 350 | /// if cfg!(windows) { 351 | /// assert_eq!( 352 | /// Path::new(r"X:\foo"), 353 | /// BasePath::try_new(r"X:\foo\bar").unwrap().parent()?.unwrap(), 354 | /// ); 355 | /// } 356 | /// # 357 | /// # Ok::<_, ParentError>(()) 358 | /// ``` 359 | /// 360 | /// [normalized]: Self::normalize 361 | /// [`parent_unchecked`]: Self::parent_unchecked 362 | #[inline] 363 | pub fn parent(&self) -> Result, ParentError> { 364 | self.check_parent().map(|()| self.parent_unchecked()) 365 | } 366 | 367 | /// Equivalent to [`Path::parent`]. 368 | /// 369 | /// It is usually better to use [`parent`]. 370 | /// 371 | /// # Examples 372 | /// 373 | /// ``` 374 | /// use std::path::Path; 375 | /// 376 | /// use normpath::BasePath; 377 | /// 378 | /// if cfg!(windows) { 379 | /// assert_eq!( 380 | /// Path::new(r"X:\foo"), 381 | /// BasePath::try_new(r"X:\foo\..") 382 | /// .unwrap() 383 | /// .parent_unchecked() 384 | /// .unwrap(), 385 | /// ); 386 | /// } 387 | /// ``` 388 | /// 389 | /// [`parent`]: Self::parent 390 | #[inline] 391 | #[must_use] 392 | pub fn parent_unchecked(&self) -> Option<&Self> { 393 | self.as_path() 394 | .parent() 395 | .map(|x| Self::from_inner(x.as_os_str())) 396 | } 397 | 398 | /// Equivalent to [`Path::read_dir`]. 399 | #[inline] 400 | pub fn read_dir(&self) -> io::Result { 401 | self.as_path().read_dir() 402 | } 403 | 404 | /// Equivalent to [`Path::read_link`]. 405 | #[inline] 406 | pub fn read_link(&self) -> io::Result { 407 | self.as_path().read_link() 408 | } 409 | 410 | /// Equivalent to [`PathExt::shorten`]. 411 | #[inline] 412 | pub fn shorten(&self) -> io::Result> { 413 | self.as_path().shorten().map(cow_path_into_base_path) 414 | } 415 | 416 | /// Equivalent to [`Path::starts_with`]. 417 | #[inline] 418 | #[must_use] 419 | pub fn starts_with

(&self, base: P) -> bool 420 | where 421 | P: AsRef, 422 | { 423 | self.as_path().starts_with(base) 424 | } 425 | 426 | /// Equivalent to [`Path::symlink_metadata`]. 427 | #[inline] 428 | pub fn symlink_metadata(&self) -> io::Result { 429 | self.as_path().symlink_metadata() 430 | } 431 | 432 | /// Equivalent to [`Path::try_exists`]. 433 | #[inline] 434 | pub fn try_exists(&self) -> io::Result { 435 | self.as_path().try_exists() 436 | } 437 | } 438 | 439 | impl AsRef for BasePath { 440 | #[inline] 441 | fn as_ref(&self) -> &OsStr { 442 | &self.0 443 | } 444 | } 445 | 446 | impl AsRef for BasePath { 447 | #[inline] 448 | fn as_ref(&self) -> &Path { 449 | self.as_path() 450 | } 451 | } 452 | 453 | impl AsRef for BasePath { 454 | #[inline] 455 | fn as_ref(&self) -> &Self { 456 | self 457 | } 458 | } 459 | 460 | impl<'a> From<&'a BasePath> for Cow<'a, BasePath> { 461 | #[inline] 462 | fn from(value: &'a BasePath) -> Self { 463 | Cow::Borrowed(value) 464 | } 465 | } 466 | 467 | impl PartialEq for BasePath { 468 | #[inline] 469 | fn eq(&self, other: &Path) -> bool { 470 | &self.0 == other.as_os_str() 471 | } 472 | } 473 | 474 | impl PartialEq for Path { 475 | #[inline] 476 | fn eq(&self, other: &BasePath) -> bool { 477 | other == self 478 | } 479 | } 480 | 481 | impl PartialOrd for BasePath { 482 | #[inline] 483 | fn partial_cmp(&self, other: &Path) -> Option { 484 | self.0.partial_cmp(other.as_os_str()) 485 | } 486 | } 487 | 488 | impl PartialOrd for Path { 489 | #[inline] 490 | fn partial_cmp(&self, other: &BasePath) -> Option { 491 | other.partial_cmp(self) 492 | } 493 | } 494 | 495 | impl ToOwned for BasePath { 496 | type Owned = BasePathBuf; 497 | 498 | #[inline] 499 | fn to_owned(&self) -> Self::Owned { 500 | BasePathBuf(self.0.to_owned().into()) 501 | } 502 | } 503 | 504 | /// An owned path that has a [prefix] on Windows. 505 | /// 506 | /// For more information, see [`BasePath`]. 507 | /// 508 | /// [prefix]: ::std::path::Prefix 509 | #[derive(Clone, Debug)] 510 | pub struct BasePathBuf(pub(super) PathBuf); 511 | 512 | impl BasePathBuf { 513 | /// Equivalent to [`BasePath::new`] but returns an owned path. 514 | /// 515 | /// # Examples 516 | /// 517 | /// ``` 518 | /// # use std::io; 519 | /// use std::path::Path; 520 | /// 521 | /// use normpath::BasePathBuf; 522 | /// 523 | /// if cfg!(windows) { 524 | /// let path = r"X:\foo\bar"; 525 | /// assert_eq!(Path::new(path), BasePathBuf::new(path)?); 526 | /// 527 | /// assert!(BasePathBuf::new(r"foo\bar").is_ok()); 528 | /// } 529 | /// # 530 | /// # Ok::<_, io::Error>(()) 531 | /// ``` 532 | #[inline] 533 | pub fn new

(path: P) -> io::Result 534 | where 535 | P: Into, 536 | { 537 | Self::try_new(path).or_else(|x| imp::to_base(&x.0)) 538 | } 539 | 540 | /// Equivalent to [`BasePath::try_new`] but returns an owned path. 541 | /// 542 | /// # Examples 543 | /// 544 | /// ``` 545 | /// use std::path::Path; 546 | /// 547 | /// # use normpath::error::MissingPrefixBufError; 548 | /// use normpath::BasePathBuf; 549 | /// 550 | /// if cfg!(windows) { 551 | /// let path = r"X:\foo\bar"; 552 | /// assert_eq!(Path::new(path), BasePathBuf::try_new(path)?); 553 | /// 554 | /// assert!(BasePathBuf::try_new(r"foo\bar").is_err()); 555 | /// } 556 | /// # 557 | /// # Ok::<_, MissingPrefixBufError>(()) 558 | /// ``` 559 | #[inline] 560 | pub fn try_new

(path: P) -> Result 561 | where 562 | P: Into, 563 | { 564 | let path = path.into(); 565 | if imp::is_base(&path) { 566 | Ok(Self(path)) 567 | } else { 568 | Err(MissingPrefixBufError(path)) 569 | } 570 | } 571 | 572 | /// Returns the wrapped path as a platform string. 573 | #[inline] 574 | #[must_use] 575 | pub fn into_os_string(self) -> OsString { 576 | self.0.into_os_string() 577 | } 578 | 579 | /// Returns the wrapped path. 580 | #[inline] 581 | #[must_use] 582 | pub fn into_path_buf(self) -> PathBuf { 583 | self.0 584 | } 585 | 586 | /// Equivalent to [`BasePath::parent`] but modifies `self` in place. 587 | /// 588 | /// Returns `Ok(false)` when [`BasePath::parent`] returns `Ok(None)`. 589 | /// 590 | /// # Examples 591 | /// 592 | /// ``` 593 | /// use std::path::Path; 594 | /// 595 | /// # use normpath::error::ParentError; 596 | /// use normpath::BasePathBuf; 597 | /// 598 | /// if cfg!(windows) { 599 | /// let mut path = BasePathBuf::try_new(r"X:\foo\bar").unwrap(); 600 | /// assert!(path.pop()?); 601 | /// assert_eq!(Path::new(r"X:\foo"), path); 602 | /// } 603 | /// # 604 | /// # Ok::<_, ParentError>(()) 605 | /// ``` 606 | #[inline] 607 | pub fn pop(&mut self) -> Result { 608 | self.check_parent().map(|()| self.pop_unchecked()) 609 | } 610 | 611 | /// Equivalent to [`PathBuf::pop`]. 612 | /// 613 | /// It is usually better to use [`pop`]. 614 | /// 615 | /// # Examples 616 | /// 617 | /// ``` 618 | /// use std::path::Path; 619 | /// 620 | /// use normpath::BasePathBuf; 621 | /// 622 | /// if cfg!(windows) { 623 | /// let mut path = BasePathBuf::try_new(r"X:\foo\..").unwrap(); 624 | /// assert!(path.pop_unchecked()); 625 | /// assert_eq!(Path::new(r"X:\foo"), path); 626 | /// } 627 | /// ``` 628 | /// 629 | /// [`pop`]: Self::pop 630 | #[inline] 631 | pub fn pop_unchecked(&mut self) -> bool { 632 | self.0.pop() 633 | } 634 | 635 | /// Equivalent to [`BasePath::join`] but modifies `self` in place. 636 | /// 637 | /// # Examples 638 | /// 639 | /// ``` 640 | /// use std::path::Path; 641 | /// 642 | /// use normpath::BasePathBuf; 643 | /// 644 | /// if cfg!(windows) { 645 | /// let mut path = BasePathBuf::try_new(r"\\?\foo\bar").unwrap(); 646 | /// path.push("../baz/test.rs"); 647 | /// assert_eq!(Path::new(r"\\?\foo\baz\test.rs"), path); 648 | /// } 649 | /// ``` 650 | #[inline] 651 | pub fn push

(&mut self, path: P) 652 | where 653 | P: AsRef, 654 | { 655 | imp::push(self, path.as_ref()); 656 | } 657 | } 658 | 659 | impl AsRef for BasePathBuf { 660 | #[inline] 661 | fn as_ref(&self) -> &OsStr { 662 | self.as_os_str() 663 | } 664 | } 665 | 666 | impl AsRef for BasePathBuf { 667 | #[inline] 668 | fn as_ref(&self) -> &Path { 669 | &self.0 670 | } 671 | } 672 | 673 | impl AsRef for BasePathBuf { 674 | #[inline] 675 | fn as_ref(&self) -> &BasePath { 676 | self 677 | } 678 | } 679 | 680 | impl Borrow for BasePathBuf { 681 | #[inline] 682 | fn borrow(&self) -> &BasePath { 683 | self 684 | } 685 | } 686 | 687 | impl Deref for BasePathBuf { 688 | type Target = BasePath; 689 | 690 | #[inline] 691 | fn deref(&self) -> &BasePath { 692 | BasePath::from_inner(self.0.as_os_str()) 693 | } 694 | } 695 | 696 | impl Eq for BasePathBuf {} 697 | 698 | impl From for Cow<'_, BasePath> { 699 | #[inline] 700 | fn from(value: BasePathBuf) -> Self { 701 | Cow::Owned(value) 702 | } 703 | } 704 | 705 | impl From for OsString { 706 | #[inline] 707 | fn from(value: BasePathBuf) -> Self { 708 | value.into_os_string() 709 | } 710 | } 711 | 712 | impl From for PathBuf { 713 | #[inline] 714 | fn from(value: BasePathBuf) -> Self { 715 | value.0 716 | } 717 | } 718 | 719 | impl Hash for BasePathBuf { 720 | #[inline] 721 | fn hash(&self, state: &mut H) 722 | where 723 | H: Hasher, 724 | { 725 | (**self).hash(state); 726 | } 727 | } 728 | 729 | impl Ord for BasePathBuf { 730 | #[inline] 731 | fn cmp(&self, other: &Self) -> Ordering { 732 | (**self).cmp(&**other) 733 | } 734 | } 735 | 736 | impl PartialEq for BasePathBuf { 737 | #[inline] 738 | fn eq(&self, other: &Self) -> bool { 739 | **self == **other 740 | } 741 | } 742 | 743 | impl PartialOrd for BasePathBuf { 744 | #[allow(clippy::non_canonical_partial_ord_impl)] 745 | #[inline] 746 | fn partial_cmp(&self, other: &Self) -> Option { 747 | (**self).partial_cmp(&**other) 748 | } 749 | } 750 | 751 | #[cfg(feature = "print_bytes")] 752 | #[cfg_attr(normpath_docs_rs, doc(cfg(feature = "print_bytes")))] 753 | mod print_bytes { 754 | use print_bytes::ByteStr; 755 | use print_bytes::ToBytes; 756 | #[cfg(windows)] 757 | use print_bytes::WideStr; 758 | 759 | use super::BasePath; 760 | use super::BasePathBuf; 761 | 762 | impl ToBytes for BasePath { 763 | #[inline] 764 | fn to_bytes(&self) -> ByteStr<'_> { 765 | self.0.to_bytes() 766 | } 767 | 768 | #[cfg(windows)] 769 | #[inline] 770 | fn to_wide(&self) -> Option { 771 | self.0.to_wide() 772 | } 773 | } 774 | 775 | impl ToBytes for BasePathBuf { 776 | #[inline] 777 | fn to_bytes(&self) -> ByteStr<'_> { 778 | (**self).to_bytes() 779 | } 780 | 781 | #[cfg(windows)] 782 | #[inline] 783 | fn to_wide(&self) -> Option { 784 | (**self).to_wide() 785 | } 786 | } 787 | } 788 | 789 | #[cfg(feature = "serde")] 790 | #[cfg_attr(normpath_docs_rs, doc(cfg(feature = "serde")))] 791 | mod serde { 792 | use std::ffi::OsString; 793 | 794 | use serde::Deserialize; 795 | use serde::Deserializer; 796 | use serde::Serialize; 797 | use serde::Serializer; 798 | 799 | use super::BasePath; 800 | use super::BasePathBuf; 801 | 802 | impl<'de> Deserialize<'de> for BasePathBuf { 803 | #[inline] 804 | fn deserialize(deserializer: D) -> Result 805 | where 806 | D: Deserializer<'de>, 807 | { 808 | OsString::deserialize(deserializer).map(|x| Self(x.into())) 809 | } 810 | } 811 | 812 | impl Serialize for BasePath { 813 | #[inline] 814 | fn serialize(&self, serializer: S) -> Result 815 | where 816 | S: Serializer, 817 | { 818 | serializer.serialize_newtype_struct("BasePath", &self.0) 819 | } 820 | } 821 | 822 | impl Serialize for BasePathBuf { 823 | #[inline] 824 | fn serialize(&self, serializer: S) -> Result 825 | where 826 | S: Serializer, 827 | { 828 | serializer 829 | .serialize_newtype_struct("BasePathBuf", self.as_os_str()) 830 | } 831 | } 832 | } 833 | 834 | #[cfg(feature = "uniquote")] 835 | #[cfg_attr(normpath_docs_rs, doc(cfg(feature = "uniquote")))] 836 | mod uniquote { 837 | use uniquote::Formatter; 838 | use uniquote::Quote; 839 | use uniquote::Result; 840 | 841 | use super::BasePath; 842 | use super::BasePathBuf; 843 | 844 | impl Quote for BasePath { 845 | #[inline] 846 | fn escape(&self, f: &mut Formatter<'_>) -> Result { 847 | self.0.escape(f) 848 | } 849 | } 850 | 851 | impl Quote for BasePathBuf { 852 | #[inline] 853 | fn escape(&self, f: &mut Formatter<'_>) -> Result { 854 | (**self).escape(f) 855 | } 856 | } 857 | } 858 | -------------------------------------------------------------------------------- /src/cmp.rs: -------------------------------------------------------------------------------- 1 | //! Implementations of comparison traits copied and modified from The Rust 2 | //! Programming Language. 3 | //! 4 | //! Sources: 5 | //! - 6 | //! 7 | //! Copyrights: 8 | //! - Copyrights in the Rust project are retained by their contributors. No 9 | //! copyright assignment is required to contribute to the Rust project. 10 | //! 11 | //! Some files include explicit copyright notices and/or license notices. 12 | //! For full authorship information, see the version control history or 13 | //! 14 | //! 15 | //! 16 | //! - Modifications copyright (c) 2020 dylni ()
17 | //! 18 | 19 | use std::borrow::Cow; 20 | use std::cmp::Ordering; 21 | use std::path::Path; 22 | use std::path::PathBuf; 23 | 24 | use super::BasePath; 25 | use super::BasePathBuf; 26 | 27 | macro_rules! r#impl { 28 | ( $left:ty , $right:ty ) => { 29 | impl PartialEq<$right> for $left { 30 | #[inline] 31 | fn eq(&self, other: &$right) -> bool { 32 | >::eq(self, other.as_ref()) 33 | } 34 | } 35 | 36 | impl PartialEq<$left> for $right { 37 | #[inline] 38 | fn eq(&self, other: &$left) -> bool { 39 | other == self 40 | } 41 | } 42 | 43 | impl PartialOrd<$right> for $left { 44 | #[inline] 45 | fn partial_cmp(&self, other: &$right) -> Option { 46 | >::partial_cmp( 47 | self, 48 | other.as_ref(), 49 | ) 50 | } 51 | } 52 | 53 | impl PartialOrd<$left> for $right { 54 | #[inline] 55 | fn partial_cmp(&self, other: &$left) -> Option { 56 | other.partial_cmp(self) 57 | } 58 | } 59 | }; 60 | } 61 | 62 | r#impl!(BasePathBuf, BasePath); 63 | r#impl!(BasePathBuf, &BasePath); 64 | r#impl!(Cow<'_, BasePath>, BasePath); 65 | r#impl!(Cow<'_, BasePath>, &BasePath); 66 | r#impl!(Cow<'_, BasePath>, BasePathBuf); 67 | 68 | r#impl!(BasePathBuf, Path); 69 | r#impl!(BasePathBuf, &Path); 70 | r#impl!(BasePathBuf, Cow<'_, Path>); 71 | r#impl!(BasePathBuf, PathBuf); 72 | r#impl!(BasePath, &Path); 73 | r#impl!(BasePath, Cow<'_, Path>); 74 | r#impl!(BasePath, PathBuf); 75 | r#impl!(&BasePath, Path); 76 | r#impl!(&BasePath, Cow<'_, Path>); 77 | r#impl!(&BasePath, PathBuf); 78 | -------------------------------------------------------------------------------- /src/common/localize/macos/fruity.rs: -------------------------------------------------------------------------------- 1 | //! Implementations copied and modified from Fruity. 2 | //! 3 | //! Sources: 4 | //! - 5 | //! 6 | //! Copyrights: 7 | //! - Copyright (c) 2020 Nikolai Vazquez
8 | //! 9 | //! - Modifications copyright (c) 2020 dylni ()
10 | //! 11 | 12 | use std::os::raw::c_char; 13 | 14 | macro_rules! __stringify_sel { 15 | ( $name:ident ) => { 16 | ::std::stringify!($name) 17 | }; 18 | ( $($name:ident :)+ ) => { 19 | ::std::concat!($(::std::stringify!($name), ":"),+) 20 | }; 21 | } 22 | 23 | macro_rules! selector { 24 | ( $($token:tt)* ) => {{ 25 | let sel: *const _ = ::std::concat!(__stringify_sel!($($token)*), "\0"); 26 | unsafe { 27 | $crate::localize::macos::fruity::sel_registerName(sel.cast()) 28 | } 29 | }}; 30 | } 31 | 32 | #[link(name = "Foundation", kind = "framework")] 33 | extern "C" {} 34 | 35 | extern "C" { 36 | pub(super) fn sel_registerName(name: *const c_char) -> objc::SEL; 37 | } 38 | 39 | pub(super) mod objc { 40 | use std::cell::UnsafeCell; 41 | use std::ops::Deref; 42 | use std::os::raw::c_char; 43 | use std::os::raw::c_void; 44 | use std::ptr::NonNull; 45 | 46 | #[allow(clippy::upper_case_acronyms)] 47 | pub(super) type BOOL = c_char; 48 | 49 | #[allow(clippy::upper_case_acronyms)] 50 | pub(super) const NO: BOOL = 0; 51 | 52 | pub(in super::super) type NSUInteger = usize; 53 | 54 | #[allow(clippy::upper_case_acronyms)] 55 | #[repr(transparent)] 56 | pub(in super::super) struct SEL(NonNull); 57 | 58 | #[repr(C)] 59 | pub(in super::super) struct Object(UnsafeCell<[u8; 0]>); 60 | 61 | #[repr(C)] 62 | pub(in super::super) struct Class(Object); 63 | 64 | impl Class { 65 | pub(super) fn alloc(&self) -> NSObject { 66 | extern "C" { 67 | fn objc_msgSend(obj: &Class, sel: SEL) -> NSObject; 68 | } 69 | 70 | let sel = selector!(alloc); 71 | 72 | unsafe { objc_msgSend(self, sel) } 73 | } 74 | } 75 | 76 | #[allow(non_camel_case_types)] 77 | #[repr(transparent)] 78 | pub(in super::super) struct id(NonNull); 79 | 80 | impl Deref for id { 81 | type Target = Object; 82 | 83 | fn deref(&self) -> &Self::Target { 84 | unsafe { self.0.as_ref() } 85 | } 86 | } 87 | 88 | impl Drop for id { 89 | fn drop(&mut self) { 90 | extern "C" { 91 | fn objc_release(obj: &Object); 92 | } 93 | 94 | unsafe { objc_release(self) } 95 | } 96 | } 97 | 98 | #[repr(transparent)] 99 | pub(in super::super) struct NSObject(id); 100 | 101 | impl Deref for NSObject { 102 | type Target = id; 103 | 104 | fn deref(&self) -> &Self::Target { 105 | &self.0 106 | } 107 | } 108 | } 109 | 110 | pub(super) mod foundation { 111 | use std::ops::Deref; 112 | 113 | use super::objc::Class; 114 | use super::objc::NSObject; 115 | use super::objc::NSUInteger; 116 | use super::objc::BOOL; 117 | use super::objc::NO; 118 | use super::objc::SEL; 119 | 120 | #[repr(transparent)] 121 | pub(in super::super) struct NSStringEncoding(NSUInteger); 122 | 123 | impl NSStringEncoding { 124 | pub(in super::super) const UTF8: Self = Self(4); 125 | } 126 | 127 | #[repr(transparent)] 128 | pub(in super::super) struct NSString(NSObject); 129 | 130 | impl NSString { 131 | fn class() -> &'static Class { 132 | extern "C" { 133 | #[link_name = "OBJC_CLASS_$_NSString"] 134 | static CLASS: Class; 135 | } 136 | unsafe { &CLASS } 137 | } 138 | 139 | pub(in super::super) unsafe fn from_str_no_copy(string: &str) -> Self { 140 | extern "C" { 141 | fn objc_msgSend( 142 | obj: NSString, 143 | sel: SEL, 144 | bytes: *const u8, 145 | length: NSUInteger, 146 | encoding: NSStringEncoding, 147 | free_when_done: BOOL, 148 | ) -> NSString; 149 | } 150 | 151 | let obj = Self(Self::class().alloc()); 152 | let sel = 153 | selector!(initWithBytesNoCopy:length:encoding:freeWhenDone:); 154 | let bytes = string.as_ptr(); 155 | let length = string.len(); 156 | let encoding = NSStringEncoding::UTF8; 157 | let free_when_done = NO; 158 | 159 | unsafe { 160 | objc_msgSend(obj, sel, bytes, length, encoding, free_when_done) 161 | } 162 | } 163 | } 164 | 165 | impl Deref for NSString { 166 | type Target = NSObject; 167 | 168 | fn deref(&self) -> &Self::Target { 169 | &self.0 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/common/localize/macos/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clashing_extern_declarations)] 2 | 3 | use std::ops::Deref; 4 | use std::os::raw::c_char; 5 | use std::slice; 6 | use std::str; 7 | 8 | #[macro_use] 9 | mod fruity; 10 | use fruity::foundation::NSString; 11 | use fruity::foundation::NSStringEncoding; 12 | use fruity::objc::Class; 13 | use fruity::objc::NSObject; 14 | use fruity::objc::NSUInteger; 15 | use fruity::objc::Object; 16 | use fruity::objc::SEL; 17 | 18 | impl NSString { 19 | fn is_empty(&self) -> bool { 20 | extern "C" { 21 | fn objc_msgSend(obj: &Object, sel: SEL) -> NSUInteger; 22 | } 23 | 24 | let sel = selector!(length); 25 | 26 | let length = unsafe { objc_msgSend(self, sel) }; 27 | length == 0 28 | } 29 | 30 | fn utf8_length(&self) -> usize { 31 | extern "C" { 32 | fn objc_msgSend( 33 | obj: &Object, 34 | sel: SEL, 35 | enc: NSStringEncoding, 36 | ) -> NSUInteger; 37 | } 38 | 39 | let sel = selector!(lengthOfBytesUsingEncoding:); 40 | 41 | let length = 42 | unsafe { objc_msgSend(self, sel, NSStringEncoding::UTF8) }; 43 | if length == 0 { 44 | assert!(self.is_empty()); 45 | } 46 | length 47 | } 48 | 49 | fn to_utf8_ptr(&self) -> *const u8 { 50 | extern "C" { 51 | fn objc_msgSend(obj: &Object, sel: SEL) -> *const c_char; 52 | } 53 | 54 | let sel = selector!(UTF8String); 55 | 56 | unsafe { objc_msgSend(self, sel) }.cast() 57 | } 58 | 59 | unsafe fn to_str(&self) -> &str { 60 | let length = self.utf8_length(); 61 | // SAFETY: These bytes are encoded using UTF-8. 62 | unsafe { 63 | str::from_utf8_unchecked(slice::from_raw_parts( 64 | self.to_utf8_ptr(), 65 | length, 66 | )) 67 | } 68 | } 69 | } 70 | 71 | impl ToString for NSString { 72 | fn to_string(&self) -> String { 73 | // SAFETY: The string has a short lifetime. 74 | unsafe { self.to_str() }.to_owned() 75 | } 76 | } 77 | 78 | #[repr(transparent)] 79 | struct NSFileManager(NSObject); 80 | 81 | impl NSFileManager { 82 | fn class() -> &'static Class { 83 | extern "C" { 84 | #[link_name = "OBJC_CLASS_$_NSFileManager"] 85 | static CLASS: Class; 86 | } 87 | unsafe { &CLASS } 88 | } 89 | 90 | fn default() -> Self { 91 | extern "C" { 92 | fn objc_msgSend(obj: &Class, sel: SEL) -> &Object; 93 | 94 | fn objc_retain(obj: &Object) -> NSFileManager; 95 | } 96 | 97 | let obj = Self::class(); 98 | let sel = selector!(defaultManager); 99 | 100 | unsafe { objc_retain(objc_msgSend(obj, sel)) } 101 | } 102 | } 103 | 104 | impl Deref for NSFileManager { 105 | type Target = NSObject; 106 | 107 | fn deref(&self) -> &Self::Target { 108 | &self.0 109 | } 110 | } 111 | 112 | pub(super) fn name(path: &str) -> String { 113 | extern "C" { 114 | fn objc_msgSend(obj: &Object, sel: SEL, path: NSString) -> &Object; 115 | 116 | fn objc_retain(obj: &Object) -> NSString; 117 | } 118 | 119 | let obj = NSFileManager::default(); 120 | let sel = selector!(displayNameAtPath:); 121 | // SAFETY: This struct is dropped by the end of this method. 122 | let path = unsafe { NSString::from_str_no_copy(path) }; 123 | 124 | unsafe { objc_retain(objc_msgSend(&obj, sel, path)) }.to_string() 125 | } 126 | -------------------------------------------------------------------------------- /src/common/localize/mod.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsString; 2 | use std::path::Path; 3 | 4 | #[cfg(any(target_os = "ios", target_os = "macos"))] 5 | mod macos; 6 | 7 | #[cfg_attr( 8 | not(any(target_os = "ios", target_os = "macos")), 9 | allow(unused_variables) 10 | )] 11 | pub(crate) fn name(path: &Path) -> Option { 12 | // Only UTF-8 paths can be localized on MacOS. 13 | #[cfg(any(target_os = "ios", target_os = "macos"))] 14 | if let Some(path) = path.to_str() { 15 | return Some(macos::name(path).into()); 16 | } 17 | None 18 | } 19 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::io; 3 | use std::path::Path; 4 | 5 | use crate::BasePathBuf; 6 | 7 | #[cfg(feature = "localization")] 8 | pub(super) mod localize; 9 | 10 | #[inline(always)] 11 | pub(crate) fn is_base(_: &Path) -> bool { 12 | true 13 | } 14 | 15 | #[inline(always)] 16 | pub(crate) fn to_base(_: &Path) -> io::Result { 17 | unreachable!(); 18 | } 19 | 20 | pub(crate) fn normalize(path: &Path) -> io::Result { 21 | // This method rejects null bytes and empty paths, which is consistent with 22 | // [GetFullPathNameW] on Windows. 23 | path.canonicalize().and_then(BasePathBuf::new) 24 | } 25 | 26 | pub(crate) fn expand(path: &Path) -> io::Result> { 27 | path.metadata().map(|_| Cow::Borrowed(path)) 28 | } 29 | 30 | pub(crate) fn shorten(path: &Path) -> io::Result> { 31 | expand(path) 32 | } 33 | 34 | pub(crate) fn push(base: &mut BasePathBuf, path: &Path) { 35 | if !path.as_os_str().is_empty() { 36 | base.0.push(path); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! The error types defined by this crate. 2 | 3 | use std::error::Error; 4 | use std::fmt; 5 | use std::fmt::Display; 6 | use std::fmt::Formatter; 7 | use std::path::Path; 8 | use std::path::PathBuf; 9 | 10 | /// The error returned when [`BasePath::try_new`] is given a path without a 11 | /// prefix. 12 | /// 13 | /// [`BasePath::try_new`]: super::BasePath::try_new 14 | #[derive(Clone, Debug, PartialEq)] 15 | pub struct MissingPrefixError(pub(super) ()); 16 | 17 | impl Display for MissingPrefixError { 18 | #[inline] 19 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 20 | "path is missing a prefix".fmt(f) 21 | } 22 | } 23 | 24 | impl Error for MissingPrefixError {} 25 | 26 | /// The error returned when [`BasePathBuf::try_new`] is given a path without a 27 | /// prefix. 28 | /// 29 | /// [`BasePathBuf::try_new`]: super::BasePathBuf::try_new 30 | #[derive(Clone, Debug, PartialEq)] 31 | pub struct MissingPrefixBufError(pub(super) PathBuf); 32 | 33 | impl MissingPrefixBufError { 34 | /// Returns a reference to the path that caused this error. 35 | #[inline] 36 | #[must_use] 37 | pub fn as_path(&self) -> &Path { 38 | &self.0 39 | } 40 | 41 | /// Returns the path that caused this error. 42 | #[inline] 43 | #[must_use] 44 | pub fn into_path_buf(self) -> PathBuf { 45 | self.0 46 | } 47 | } 48 | 49 | impl Display for MissingPrefixBufError { 50 | #[inline] 51 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 52 | write!(f, "path is missing a prefix: \"{}\"", self.0.display()) 53 | } 54 | } 55 | 56 | impl Error for MissingPrefixBufError {} 57 | 58 | /// The error returned when [`BasePath::parent`] cannot remove the path's last 59 | /// component. 60 | /// 61 | /// [`BasePath::parent`]: super::BasePath::parent 62 | #[derive(Clone, Debug, PartialEq)] 63 | pub struct ParentError(pub(super) ()); 64 | 65 | impl Display for ParentError { 66 | #[inline] 67 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 68 | "cannot remove the path's last component".fmt(f) 69 | } 70 | } 71 | 72 | impl Error for ParentError {} 73 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides methods to normalize paths in the recommended way for 2 | //! the operating system. 3 | //! 4 | //! It was made to fix a recurring bug caused by using [`fs::canonicalize`] on 5 | //! Windows: [#45067], [#48249], [#52440], [#55812], [#58613], [#59107], 6 | //! [#74327]. Normalization is usually a better choice unless you specifically 7 | //! need a canonical path. 8 | //! 9 | //! Using these replacement methods will usually fix those issues, but see 10 | //! their documentation for more information: 11 | //! - [`PathExt::normalize`] (*usually* replaces [`Path::canonicalize`]) 12 | //! - [`BasePath::join`] (replaces [`Path::join`]) 13 | //! - [`BasePath::parent`] (replaces [`Path::parent`]) 14 | //! - [`BasePathBuf::pop`] (replaces [`PathBuf::pop`]) 15 | //! - [`BasePathBuf::push`] (replaces [`PathBuf::push`]) 16 | //! 17 | //! Additionally, these methods can be used for other enhancements: 18 | //! - [`PathExt::localize_name`] 19 | //! 20 | //! # Features 21 | //! 22 | //! These features are optional and can be enabled or disabled in a 23 | //! "Cargo.toml" file. 24 | //! 25 | //! ### Optional Features 26 | //! 27 | //! - **localization** - 28 | //! Provides [`PathExt::localize_name`] and [`BasePath::localize_name`]. 29 | //! 30 | //! - **print\_bytes** - 31 | //! Provides implementations of [`print_bytes::ToBytes`] for [`BasePath`] and 32 | //! [`BasePathBuf`]. 33 | //! 34 | //! - **serde** - 35 | //! Provides implementations of [`serde::Deserialize`] and/or 36 | //! [`serde::Serialize`] for [`BasePath`] and [`BasePathBuf`]. 37 | //! 38 | //! - **uniquote** - 39 | //! Provides implementations of [`uniquote::Quote`] for [`BasePath`] and 40 | //! [`BasePathBuf`]. 41 | //! 42 | //! # Implementation 43 | //! 44 | //! Some methods return [`Cow`] to account for platform differences. However, 45 | //! no guarantee is made that the same variant of that enum will always be 46 | //! returned for the same platform. Whichever can be constructed most 47 | //! efficiently will be returned. 48 | //! 49 | //! All traits are [sealed], meaning that they can only be implemented by this 50 | //! crate. Otherwise, backward compatibility would be more difficult to 51 | //! maintain for new features. 52 | //! 53 | //! # Sponsorship 54 | //! 55 | //! If this crate has been useful for your project, let me know with a 56 | //! [sponsorship](https://github.com/sponsors/dylni)! Sponsorships help me 57 | //! create and maintain my open source libraries, and they are always very 58 | //! appreciated. 59 | //! 60 | //! # Examples 61 | //! 62 | //! ``` 63 | //! use std::io; 64 | //! use std::path::Path; 65 | //! 66 | //! use normpath::BasePathBuf; 67 | //! use normpath::PathExt; 68 | //! 69 | //! fn find_target_dir(path: &Path) -> io::Result> { 70 | //! let mut path = path.normalize()?; 71 | //! while !path.ends_with("target") { 72 | //! match path.pop() { 73 | //! Ok(true) => continue, 74 | //! Ok(false) => {} 75 | //! Err(_) => { 76 | //! eprintln!("Some components could not be normalized."); 77 | //! } 78 | //! } 79 | //! return Ok(None); 80 | //! } 81 | //! Ok(Some(path)) 82 | //! } 83 | //! ``` 84 | //! 85 | //! [#45067]: https://github.com/rust-lang/rust/issues/45067 86 | //! [#48249]: https://github.com/rust-lang/rust/issues/48249 87 | //! [#52440]: https://github.com/rust-lang/rust/issues/52440 88 | //! [#55812]: https://github.com/rust-lang/rust/issues/55812 89 | //! [#58613]: https://github.com/rust-lang/rust/issues/58613 90 | //! [#59107]: https://github.com/rust-lang/rust/issues/59107 91 | //! [#74327]: https://github.com/rust-lang/rust/issues/74327 92 | //! [`fs::canonicalize`]: ::std::fs::canonicalize 93 | //! [`PathBuf::pop`]: ::std::path::PathBuf::pop 94 | //! [`PathBuf::push`]: ::std::path::PathBuf::push 95 | //! [sealed]: https://rust-lang.github.io/api-guidelines/future-proofing.html#c-sealed 96 | 97 | // Only require a nightly compiler when building documentation for docs.rs. 98 | // This is a private option that should not be used. 99 | // https://github.com/rust-lang/docs.rs/issues/147#issuecomment-389544407 100 | #![cfg_attr(normpath_docs_rs, feature(doc_cfg))] 101 | #![warn(unused_results)] 102 | 103 | use std::borrow::Cow; 104 | #[cfg(feature = "localization")] 105 | use std::ffi::OsStr; 106 | use std::io; 107 | #[cfg(feature = "localization")] 108 | use std::path::Component; 109 | use std::path::Path; 110 | 111 | mod base; 112 | pub use base::BasePath; 113 | pub use base::BasePathBuf; 114 | 115 | mod cmp; 116 | 117 | pub mod error; 118 | 119 | #[cfg_attr(windows, path = "windows/mod.rs")] 120 | #[cfg_attr(not(windows), path = "common/mod.rs")] 121 | mod imp; 122 | #[cfg(feature = "localization")] 123 | use imp::localize; 124 | 125 | /// Additional methods added to [`Path`]. 126 | pub trait PathExt: private::Sealed { 127 | /// Expands `self` from its short form, if the convention exists for the 128 | /// platform. 129 | /// 130 | /// This method reverses [`shorten`] but may not return the original path. 131 | /// Additional components may be shortened that were not before calling 132 | /// [`shorten`]. 133 | /// 134 | /// # Implementation 135 | /// 136 | /// Currently, this method calls: 137 | /// - [`GetLongPathNameW`] on Windows. 138 | /// 139 | /// However, the implementation is subject to change. This section is only 140 | /// informative. 141 | /// 142 | /// # Errors 143 | /// 144 | /// Returns an error if `self` does not exist, even on Unix. 145 | /// 146 | /// # Examples 147 | /// 148 | /// ``` 149 | /// # use std::io; 150 | /// use std::path::Path; 151 | /// 152 | /// use normpath::PathExt; 153 | /// 154 | /// if cfg!(windows) { 155 | /// assert_eq!( 156 | /// Path::new(r"C:\Documents and Settings"), 157 | /// Path::new(r"C:\DOCUME~1").expand()?, 158 | /// ); 159 | /// } 160 | /// # 161 | /// # Ok::<_, io::Error>(()) 162 | /// ``` 163 | /// 164 | /// [`GetLongPathNameW`]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getlongpathnamew 165 | /// [`shorten`]: Self::shorten 166 | fn expand(&self) -> io::Result> 167 | where 168 | Self: ToOwned; 169 | 170 | /// Returns the localized simple name for this path. 171 | /// 172 | /// If the path does not exist or localization is not possible, the last 173 | /// component will be returned. 174 | /// 175 | /// The returned string should only be used for display to users. It will 176 | /// be as similar as possible to the name displayed by the system file 177 | /// manager for the path. However, nothing should be assumed about the 178 | /// result. 179 | /// 180 | /// # Implementation 181 | /// 182 | /// Currently, this method calls: 183 | /// 184 | ///
  • 185 | /// 186 | /// [`[NSFileManager displayNameAtPath:]`][displayNameAtPath] on MacOS 187 | /// ([rust-lang/rfcs#845]). 188 | /// 189 | ///
  • 190 | /// 191 | /// [`SHGetFileInfoW`] on Windows. 192 | /// 193 | ///
    194 | /// 195 | /// This function has a usage note in its documentation: 196 | /// 197 | ///
    198 | /// 199 | /// You should call this function from a background thread. Failure to do 200 | /// so could cause the UI to stop responding. 201 | /// 202 | ///
203 | /// 204 | /// However, the implementation is subject to change. This section is only 205 | /// informative. 206 | /// 207 | /// # Panics 208 | /// 209 | /// Panics if the path ends with a `..` component. In the future, this 210 | /// method might also panic for paths ending with `.` components, so they 211 | /// should not be given either. They currently cause a platform-dependent 212 | /// value to be returned. 213 | /// 214 | /// You should usually only call this method on [normalized] paths to avoid 215 | /// these panics. 216 | /// 217 | /// # Examples 218 | /// 219 | /// ``` 220 | /// use std::path::Path; 221 | /// 222 | /// use normpath::PathExt; 223 | /// 224 | /// assert_eq!("test.rs", &*Path::new("/foo/bar/test.rs").localize_name()); 225 | /// ``` 226 | /// 227 | /// [displayNameAtPath]: https://developer.apple.com/documentation/foundation/nsfilemanager/1409751-displaynameatpath 228 | /// [normalized]: Self::normalize 229 | /// [rust-lang/rfcs#845]: https://github.com/rust-lang/rfcs/issues/845 230 | /// [`SHGetFileInfoW`]: https://docs.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shgetfileinfow 231 | #[cfg(feature = "localization")] 232 | #[cfg_attr(normpath_docs_rs, doc(cfg(feature = "localization")))] 233 | #[must_use] 234 | fn localize_name(&self) -> Cow<'_, OsStr>; 235 | 236 | /// Normalizes `self` relative to the current directory. 237 | /// 238 | /// The purpose of normalization is to remove `.` and `..` components of a 239 | /// path if possible and make it absolute. This may be necessary for 240 | /// operations on the path string to be more reliable. 241 | /// 242 | /// This method will access the file system to normalize the path. If the 243 | /// path might not exist, [`normalize_virtually`] can be used instead, but 244 | /// it is only available on Windows. Other platforms require file system 245 | /// access to perform normalization. 246 | /// 247 | /// # Unix Behavior 248 | /// 249 | /// On Unix, normalization is equivalent to canonicalization. 250 | /// 251 | /// # Windows Behavior 252 | /// 253 | /// On Windows, normalization is similar to canonicalization, but: 254 | /// - the [prefix] of the path is rarely changed. Canonicalization would 255 | /// always return a [verbatim] path, which can be difficult to use. 256 | /// ([rust-lang/rust#42869]) 257 | /// - the result is more consistent. ([rust-lang/rust#49342]) 258 | /// - shared partition paths do not cause an error. 259 | /// ([rust-lang/rust#52440]) 260 | /// 261 | /// [Verbatim] paths will not be modified, so they might still contain `.` 262 | /// or `..` components. [`BasePath::join`] and [`BasePathBuf::push`] can 263 | /// normalize them before they become part of the path. Junction points 264 | /// will additionally not be resolved with the current implementation. 265 | /// 266 | /// # Implementation 267 | /// 268 | /// Currently, this method calls: 269 | /// - [`fs::canonicalize`] on Unix. 270 | /// - [`GetFullPathNameW`] on Windows. 271 | /// 272 | /// However, the implementation is subject to change. This section is only 273 | /// informative. 274 | /// 275 | /// # Errors 276 | /// 277 | /// Returns an error if `self` cannot be normalized or does not exist, even 278 | /// on Windows. 279 | /// 280 | /// This method is designed to give mostly consistent errors on different 281 | /// platforms, even when the functions it calls have different behavior. To 282 | /// normalize paths that might not exist, use [`normalize_virtually`]. 283 | /// 284 | /// # Examples 285 | /// 286 | /// ```no_run 287 | /// # use std::io; 288 | /// use std::path::Path; 289 | /// 290 | /// use normpath::PathExt; 291 | /// 292 | /// if cfg!(windows) { 293 | /// assert_eq!( 294 | /// Path::new(r"X:\foo\baz\test.rs"), 295 | /// Path::new("X:/foo/bar/../baz/test.rs").normalize()?, 296 | /// ); 297 | /// } 298 | /// # 299 | /// # Ok::<_, io::Error>(()) 300 | /// ``` 301 | /// 302 | /// [`fs::canonicalize`]: ::std::fs::canonicalize 303 | /// [`GetFullPathNameW`]: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getfullpathnamew 304 | /// [`normalize_virtually`]: Self::normalize_virtually 305 | /// [rust-lang/rust#42869]: https://github.com/rust-lang/rust/issues/42869 306 | /// [rust-lang/rust#49342]: https://github.com/rust-lang/rust/issues/49342 307 | /// [rust-lang/rust#52440]: https://github.com/rust-lang/rust/issues/52440 308 | /// [prefix]: ::std::path::Prefix 309 | /// [verbatim]: ::std::path::Prefix::is_verbatim 310 | fn normalize(&self) -> io::Result; 311 | 312 | /// Equivalent to [`normalize`] but does not access the file system. 313 | /// 314 | /// # Errors 315 | /// 316 | /// Returns an error if `self` cannot be normalized or contains a null 317 | /// byte. Nonexistent paths will not cause an error. 318 | /// 319 | /// # Examples 320 | /// 321 | /// ``` 322 | /// # use std::io; 323 | /// use std::path::Path; 324 | /// 325 | /// use normpath::PathExt; 326 | /// 327 | /// #[cfg(windows)] 328 | /// assert_eq!( 329 | /// Path::new(r"X:\foo\baz\test.rs"), 330 | /// Path::new("X:/foo/bar/../baz/test.rs").normalize_virtually()?, 331 | /// ); 332 | /// # 333 | /// # Ok::<_, io::Error>(()) 334 | /// ``` 335 | /// 336 | /// [`normalize`]: Self::normalize 337 | #[cfg(any(doc, windows))] 338 | #[cfg_attr(normpath_docs_rs, doc(cfg(windows)))] 339 | fn normalize_virtually(&self) -> io::Result; 340 | 341 | /// Shortens `self` from its expanded form, if the convention exists for 342 | /// the platform. 343 | /// 344 | /// This method reverses [`expand`] but may not return the original path. 345 | /// Additional components may be shortened that were not before calling 346 | /// [`expand`]. 347 | /// 348 | /// # Implementation 349 | /// 350 | /// Currently, this method calls: 351 | /// - [`GetShortPathNameW`] on Windows. 352 | /// 353 | /// However, the implementation is subject to change. This section is only 354 | /// informative. 355 | /// 356 | /// # Errors 357 | /// 358 | /// Returns an error if `self` does not exist, even on Unix. 359 | /// 360 | /// # Examples 361 | /// 362 | /// ``` 363 | /// # use std::io; 364 | /// use std::path::Path; 365 | /// 366 | /// use normpath::PathExt; 367 | /// 368 | /// if cfg!(windows) { 369 | /// assert_eq!( 370 | /// Path::new(r"C:\DOCUME~1"), 371 | /// Path::new(r"C:\Documents and Settings").shorten()?, 372 | /// ); 373 | /// } 374 | /// # 375 | /// # Ok::<_, io::Error>(()) 376 | /// ``` 377 | /// 378 | /// [`expand`]: Self::expand 379 | /// [`GetShortPathNameW`]: https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getshortpathnamew 380 | fn shorten(&self) -> io::Result> 381 | where 382 | Self: ToOwned; 383 | } 384 | 385 | impl PathExt for Path { 386 | #[inline] 387 | fn expand(&self) -> io::Result> { 388 | imp::expand(self) 389 | } 390 | 391 | #[cfg(feature = "localization")] 392 | #[inline] 393 | fn localize_name(&self) -> Cow<'_, OsStr> { 394 | let Some(name) = self.components().next_back() else { 395 | return Cow::Borrowed(OsStr::new("")); 396 | }; 397 | assert_ne!( 398 | Component::ParentDir, 399 | name, 400 | "path ends with a `..` component: \"{}\"", 401 | self.display(), 402 | ); 403 | 404 | localize::name(self) 405 | .map(Cow::Owned) 406 | .unwrap_or_else(|| Cow::Borrowed(name.as_os_str())) 407 | } 408 | 409 | #[inline] 410 | fn normalize(&self) -> io::Result { 411 | imp::normalize(self) 412 | } 413 | 414 | #[cfg(any(doc, windows))] 415 | #[inline] 416 | fn normalize_virtually(&self) -> io::Result { 417 | imp::normalize_virtually(self) 418 | } 419 | 420 | #[inline] 421 | fn shorten(&self) -> io::Result> { 422 | imp::shorten(self) 423 | } 424 | } 425 | 426 | mod private { 427 | use std::path::Path; 428 | 429 | pub trait Sealed {} 430 | impl Sealed for Path {} 431 | } 432 | -------------------------------------------------------------------------------- /src/windows/localize.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsString; 2 | use std::os::windows::ffi::OsStrExt; 3 | use std::os::windows::ffi::OsStringExt; 4 | use std::path::Path; 5 | use std::ptr; 6 | 7 | use windows_sys::Win32::UI::Shell::SHGetFileInfoW; 8 | use windows_sys::Win32::UI::Shell::SHFILEINFOW; 9 | use windows_sys::Win32::UI::Shell::SHGFI_DISPLAYNAME; 10 | 11 | pub(crate) fn name(path: &Path) -> Option { 12 | let mut path: Vec<_> = path.as_os_str().encode_wide().collect(); 13 | if path.contains(&0) { 14 | return None; 15 | } 16 | path.push(0); 17 | 18 | let mut path_info = SHFILEINFOW { 19 | hIcon: ptr::null_mut(), 20 | iIcon: 0, 21 | dwAttributes: 0, 22 | szDisplayName: [0; 260], 23 | szTypeName: [0; 80], 24 | }; 25 | let result = unsafe { 26 | SHGetFileInfoW( 27 | path.as_ptr(), 28 | 0, 29 | &mut path_info, 30 | size_of_val(&path_info) 31 | .try_into() 32 | .expect("path information too large for WinAPI"), 33 | SHGFI_DISPLAYNAME, 34 | ) 35 | }; 36 | if result == 0 { 37 | return None; 38 | } 39 | 40 | // The display name buffer has a fixed length, so it must be truncated at 41 | // the first null character. 42 | Some(OsString::from_wide( 43 | path_info 44 | .szDisplayName 45 | .split(|&x| x == 0) 46 | .next() 47 | .expect("missing null byte in display name"), 48 | )) 49 | } 50 | -------------------------------------------------------------------------------- /src/windows/mod.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::env; 3 | use std::ffi::OsStr; 4 | use std::ffi::OsString; 5 | use std::io; 6 | use std::ops::Not; 7 | use std::os::windows::ffi::OsStrExt; 8 | use std::os::windows::ffi::OsStringExt; 9 | use std::path::Component; 10 | use std::path::Path; 11 | use std::path::Prefix; 12 | use std::path::PrefixComponent; 13 | use std::ptr; 14 | 15 | use windows_sys::Win32::Storage::FileSystem::GetFullPathNameW; 16 | use windows_sys::Win32::Storage::FileSystem::GetLongPathNameW; 17 | use windows_sys::Win32::Storage::FileSystem::GetShortPathNameW; 18 | 19 | use crate::BasePath; 20 | use crate::BasePathBuf; 21 | 22 | #[cfg(feature = "localization")] 23 | pub(super) mod localize; 24 | 25 | macro_rules! static_assert { 26 | ( $condition:expr ) => { 27 | const _: () = assert!($condition, "static assertion failed"); 28 | }; 29 | } 30 | 31 | fn is_separator(byte: &u8) -> bool { 32 | [b'/', b'\\'].contains(byte) 33 | } 34 | 35 | pub(crate) fn is_base(path: &Path) -> bool { 36 | matches!(path.components().next(), Some(Component::Prefix(_))) 37 | } 38 | 39 | pub(crate) fn to_base(path: &Path) -> io::Result { 40 | let base = env::current_dir()?; 41 | debug_assert!(is_base(&base)); 42 | 43 | let mut base = BasePathBuf(base); 44 | base.push(path); 45 | Ok(base) 46 | } 47 | 48 | #[inline(always)] 49 | const fn u32_to_usize(n: u32) -> usize { 50 | // This assertion should never fail. 51 | static_assert!(size_of::() <= size_of::()); 52 | n as usize 53 | } 54 | 55 | fn winapi_buffered(mut call_fn: F) -> io::Result> 56 | where 57 | F: FnMut(*mut u16, u32) -> u32, 58 | { 59 | let mut buffer = Vec::new(); 60 | let mut capacity = 0; 61 | loop { 62 | capacity = call_fn(buffer.as_mut_ptr(), capacity); 63 | if capacity == 0 { 64 | break Err(io::Error::last_os_error()); 65 | } 66 | 67 | let length = u32_to_usize(capacity); 68 | let Some(mut additional_capacity) = 69 | length.checked_sub(buffer.capacity()) 70 | else { 71 | // SAFETY: These characters were initialized by the syscall. 72 | unsafe { 73 | buffer.set_len(length); 74 | } 75 | return Ok(buffer); 76 | }; 77 | assert_ne!(0, additional_capacity); 78 | 79 | // WinAPI can recommend an insufficient capacity that causes it to 80 | // return incorrect results, so extra space is reserved as a 81 | // workaround. 82 | let extra_capacity = 2.min(capacity.not()); 83 | capacity += extra_capacity; 84 | additional_capacity += u32_to_usize(extra_capacity); 85 | 86 | buffer.reserve(additional_capacity); 87 | } 88 | } 89 | 90 | fn winapi_path( 91 | path: &Path, 92 | call_fn: fn(*const u16, *mut u16, u32) -> u32, 93 | ) -> io::Result> { 94 | if path.as_os_str().as_encoded_bytes().contains(&0) { 95 | return Err(io::Error::new( 96 | io::ErrorKind::InvalidInput, 97 | "strings passed to WinAPI cannot contains NULs", 98 | )); 99 | } 100 | 101 | match path.components().next() { 102 | // Verbatim paths should not be modified. 103 | Some(Component::Prefix(prefix)) if prefix.kind().is_verbatim() => { 104 | return Ok(Cow::Borrowed(path)); 105 | } 106 | Some(Component::RootDir) 107 | if path 108 | .as_os_str() 109 | .as_encoded_bytes() 110 | .get(1) 111 | .is_some_and(is_separator) => 112 | { 113 | return Err(io::Error::new( 114 | io::ErrorKind::NotFound, 115 | "partial UNC prefixes are invalid", 116 | )); 117 | } 118 | _ => {} 119 | } 120 | 121 | let mut path: Vec<_> = path.as_os_str().encode_wide().collect(); 122 | debug_assert!(!path.contains(&0)); 123 | path.push(0); 124 | 125 | path = winapi_buffered(|buffer, capacity| { 126 | call_fn(path.as_ptr(), buffer, capacity) 127 | })?; 128 | 129 | Ok(Cow::Owned(OsString::from_wide(&path).into())) 130 | } 131 | 132 | pub(crate) fn normalize_virtually(path: &Path) -> io::Result { 133 | winapi_path(path, |path, buffer, capacity| unsafe { 134 | GetFullPathNameW(path, capacity, buffer, ptr::null_mut()) 135 | }) 136 | .map(|x| BasePathBuf(x.into_owned())) 137 | } 138 | 139 | pub(crate) fn normalize(path: &Path) -> io::Result { 140 | // Trigger an error for nonexistent paths for consistency with other 141 | // platforms. 142 | let _ = path.metadata()?; 143 | normalize_virtually(path) 144 | } 145 | 146 | pub(crate) fn expand(path: &Path) -> io::Result> { 147 | winapi_path(path, |path, buffer, capacity| unsafe { 148 | GetLongPathNameW(path, buffer, capacity) 149 | }) 150 | } 151 | 152 | pub(crate) fn shorten(path: &Path) -> io::Result> { 153 | winapi_path(path, |path, buffer, capacity| unsafe { 154 | GetShortPathNameW(path, buffer, capacity) 155 | }) 156 | } 157 | 158 | fn get_prefix(base: &BasePath) -> PrefixComponent<'_> { 159 | if let Some(Component::Prefix(prefix)) = base.components().next() { 160 | prefix 161 | } else { 162 | // Base paths should always have a prefix. 163 | panic!( 164 | "base path is missing a prefix: \"{}\"", 165 | base.as_path().display(), 166 | ); 167 | } 168 | } 169 | 170 | fn convert_separators(path: &Path) -> Cow<'_, OsStr> { 171 | let path_bytes = path.as_os_str().as_encoded_bytes(); 172 | let mut parts = path_bytes.split(|&x| x == b'/'); 173 | 174 | let part = parts.next().expect("split iterator is empty"); 175 | if part.len() == path_bytes.len() { 176 | debug_assert_eq!(path_bytes, part); 177 | debug_assert_eq!(None, parts.next()); 178 | return Cow::Borrowed(path.as_os_str()); 179 | } 180 | 181 | let mut path_bytes = Vec::with_capacity(path_bytes.len()); 182 | path_bytes.extend(part); 183 | for part in parts { 184 | path_bytes.push(b'\\'); 185 | path_bytes.extend(part); 186 | } 187 | 188 | // SAFETY: Only UTF-8 substrings were replaced. 189 | Cow::Owned(unsafe { OsString::from_encoded_bytes_unchecked(path_bytes) }) 190 | } 191 | 192 | fn push_separator(base: &mut BasePathBuf) { 193 | // Add a separator if necessary. 194 | base.0.push(""); 195 | } 196 | 197 | pub(crate) fn push(base: &mut BasePathBuf, path: &Path) { 198 | let mut components = path.components(); 199 | let mut next_component = components.next(); 200 | match next_component { 201 | Some(Component::Prefix(prefix)) => { 202 | // Verbatim paths should not be modified. 203 | let mut absolute = prefix.kind().is_verbatim(); 204 | if !absolute { 205 | next_component = components.next(); 206 | // Other prefixes are absolute, except drive-relative prefixes. 207 | absolute = !matches!(prefix.kind(), Prefix::Disk(_)) 208 | || prefix.kind() != get_prefix(base).kind() 209 | // Equivalent to [path.has_root()] but more efficient. 210 | || next_component == Some(Component::RootDir); 211 | } 212 | if absolute { 213 | *base = BasePathBuf(path.to_owned()); 214 | return; 215 | } 216 | } 217 | Some(Component::RootDir) => { 218 | let mut buffer = get_prefix(base).as_os_str().to_owned(); 219 | buffer.push(convert_separators(path)); 220 | *base = BasePathBuf(buffer.into()); 221 | return; 222 | } 223 | _ => { 224 | while let Some(component) = next_component { 225 | match component { 226 | Component::CurDir => {} 227 | Component::ParentDir if base.pop().is_ok() => {} 228 | _ => break, 229 | } 230 | next_component = components.next(); 231 | } 232 | } 233 | } 234 | 235 | if let Some(component) = next_component { 236 | push_separator(base); 237 | base.0.as_mut_os_string().push(component); 238 | 239 | let components = components.as_path(); 240 | if !components.as_os_str().is_empty() { 241 | push_separator(base); 242 | base.0 243 | .as_mut_os_string() 244 | .push(convert_separators(components)); 245 | } 246 | } 247 | 248 | let path_bytes = path.as_os_str().as_encoded_bytes(); 249 | // At least one separator should be kept. 250 | if path_bytes.last().is_some_and(is_separator) 251 | || matches!(path_bytes, [x, b'.'] if is_separator(x)) 252 | { 253 | push_separator(base); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /tests/common.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![allow(unused_macros)] 3 | 4 | use std::fmt; 5 | use std::fmt::Debug; 6 | use std::fmt::Formatter; 7 | use std::io; 8 | use std::path::Path; 9 | 10 | use normpath::BasePath; 11 | use normpath::BasePathBuf; 12 | use normpath::PathExt; 13 | 14 | // https://github.com/rust-lang/rust/issues/76483 15 | #[track_caller] 16 | pub(crate) fn assert_eq

(expected: &Path, result: io::Result

) 17 | where 18 | P: AsRef, 19 | { 20 | struct Wrapper<'a>(&'a Path); 21 | 22 | impl Debug for Wrapper<'_> { 23 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 24 | f.debug_tuple("Ok").field(&self.0).finish() 25 | } 26 | } 27 | 28 | impl PartialEq> for Wrapper<'_> { 29 | fn eq(&self, other: &Result<&Path, &io::Error>) -> bool { 30 | other.is_ok_and(|x| self.0.as_os_str() == x.as_os_str()) 31 | } 32 | } 33 | 34 | assert_eq!(Wrapper(expected), result.as_ref().map(AsRef::as_ref)); 35 | } 36 | 37 | pub(crate) fn normalize

(path: P) -> io::Result 38 | where 39 | P: AsRef, 40 | { 41 | let path = path.as_ref(); 42 | #[cfg(windows)] 43 | { 44 | path.normalize_virtually() 45 | } 46 | #[cfg(not(windows))] 47 | { 48 | path.normalize() 49 | } 50 | } 51 | 52 | #[track_caller] 53 | pub(crate) fn test(path: &str, joined_path: &str, normalized_path: &str) { 54 | let joined_path = Path::new(joined_path); 55 | let normalized_path = Path::new(normalized_path); 56 | 57 | let base = 58 | BasePath::try_new(if cfg!(windows) { r"X:\ABC" } else { "/tmp" }) 59 | .unwrap(); 60 | assert_eq!(joined_path, base.join(path)); 61 | 62 | assert_eq(normalized_path, normalize(joined_path)); 63 | assert_eq(normalized_path, normalize(normalized_path)); 64 | } 65 | 66 | macro_rules! test { 67 | ( $path:literal , $joined_path:literal , $normalized_path:literal ) => { 68 | $crate::common::test($path, $joined_path, $normalized_path); 69 | }; 70 | ( $path:literal , SAME, $normalized_path:literal ) => { 71 | test!($path, $path, $normalized_path); 72 | }; 73 | ( $path:literal , $joined_path:literal , SAME ) => { 74 | test!($path, $joined_path, $joined_path); 75 | }; 76 | ( $path:literal , SAME, SAME ) => { 77 | test!($path, $path, $path); 78 | }; 79 | } 80 | 81 | #[track_caller] 82 | pub(crate) fn test_join(base: &str, path: &str, result: &str) { 83 | assert_eq!( 84 | Path::new(result), 85 | BasePath::try_new(base).unwrap().join(path), 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /tests/edge_cases.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | #[cfg(windows)] 4 | #[test] 5 | fn test_windows() { 6 | use std::path::Path; 7 | 8 | use normpath::PathExt; 9 | 10 | #[track_caller] 11 | fn test(base: &str, path: &str, result: &str) { 12 | common::test_join(base, path, result); 13 | 14 | let result = Path::new(result); 15 | common::assert_eq(result, result.normalize_virtually()); 16 | } 17 | 18 | // https://github.com/dylni/normpath/pull/4#issuecomment-938596259 19 | test(r"X:\X:", r"ABC", r"X:\X:\ABC"); 20 | test(r"\\?\X:\X:", r"ABC", r"\\?\X:\X:\ABC"); 21 | } 22 | -------------------------------------------------------------------------------- /tests/integration.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs::File; 3 | use std::io; 4 | use std::path::Path; 5 | 6 | use normpath::BasePath; 7 | use normpath::PathExt; 8 | 9 | use tempfile::tempdir; 10 | 11 | #[macro_use] 12 | mod common; 13 | 14 | #[cfg(windows)] 15 | #[rustfmt::skip] 16 | use windows_sys::Win32::Foundation::ERROR_INVALID_NAME; 17 | #[cfg(not(windows))] 18 | use libc::ENOENT as ERROR_INVALID_NAME; 19 | 20 | #[test] 21 | fn test_empty() -> io::Result<()> { 22 | assert_eq!( 23 | Some(Ok(ERROR_INVALID_NAME)), 24 | common::normalize("") 25 | .unwrap_err() 26 | .raw_os_error() 27 | .map(TryInto::try_into), 28 | ); 29 | assert_eq!( 30 | io::ErrorKind::NotFound, 31 | Path::new("").normalize().unwrap_err().kind(), 32 | ); 33 | 34 | let base = env::current_dir()?; 35 | assert_eq!(&base, &BasePath::try_new(&base).unwrap().join("")); 36 | 37 | Ok(()) 38 | } 39 | 40 | #[test] 41 | fn test_created() -> io::Result<()> { 42 | let dir = tempdir()?; 43 | let dir = dir.path().normalize().unwrap(); 44 | 45 | let file = dir.as_path().join("foo"); 46 | let _ = File::create(&file)?; 47 | 48 | assert_eq!(file, dir.join("foo")); 49 | common::assert_eq(&file, file.normalize()); 50 | 51 | Ok(()) 52 | } 53 | 54 | #[cfg(feature = "serde")] 55 | #[test] 56 | fn test_serde() -> io::Result<()> { 57 | use normpath::BasePathBuf; 58 | 59 | // https://doc.rust-lang.org/std/ffi/struct.OsStr.html#examples-2 60 | let path = { 61 | #[cfg(windows)] 62 | { 63 | use std::ffi::OsString; 64 | use std::os::windows::ffi::OsStringExt; 65 | 66 | OsString::from_wide(&[0x66, 0x66, 0xD800, 0x6F]) 67 | } 68 | #[cfg(not(windows))] 69 | { 70 | use std::ffi::OsStr; 71 | use std::os::unix::ffi::OsStrExt; 72 | 73 | OsStr::from_bytes(&[0x66, 0x66, 0x80, 0x6F]).to_owned() 74 | } 75 | }; 76 | 77 | let base = BasePathBuf::new(path)?; 78 | let bytes = bincode::serialize(&base).unwrap(); 79 | assert_eq!(base, bincode::deserialize::(&bytes).unwrap()); 80 | 81 | Ok(()) 82 | } 83 | -------------------------------------------------------------------------------- /tests/localization.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "localization")] 2 | 3 | use std::path::Path; 4 | 5 | use normpath::PathExt; 6 | 7 | #[track_caller] 8 | fn test(result: &str, path: &str) { 9 | assert_eq!(result, &*Path::new(path).localize_name()); 10 | } 11 | 12 | #[test] 13 | fn test_empty() { 14 | test("", ""); 15 | } 16 | 17 | #[test] 18 | fn test_same() { 19 | test("Applications", "Applications"); 20 | test("applications", "/foo/applications"); 21 | test("applications", "/foo/applications/"); 22 | test("applications", "/foo/applications//"); 23 | if cfg!(windows) { 24 | test("applications", r"X:\foo\applications"); 25 | test("applications", r"X:\foo\applications\"); 26 | test("applications", r"X:\foo\applications\\"); 27 | } 28 | 29 | test("test\0.rs", "/foo/bar/test\0.rs"); 30 | if cfg!(unix) { 31 | test("test\\.rs", "/foo/bar/test\\.rs"); 32 | } 33 | } 34 | 35 | #[should_panic = "path ends with a `..` component: \"/foo/bar/..\""] 36 | #[test] 37 | fn test_parent() { 38 | let _ = Path::new("/foo/bar/..").localize_name(); 39 | } 40 | 41 | #[cfg(windows)] 42 | #[should_panic = r#"path ends with a `..` component: "X:\foo\bar\..""#] 43 | #[test] 44 | fn test_windows_parent() { 45 | let _ = Path::new(r"X:\foo\bar\..").localize_name(); 46 | } 47 | 48 | #[cfg(any(target_os = "ios", target_os = "macos"))] 49 | #[test] 50 | fn test_localized() { 51 | test("Applications", "/applications"); 52 | test("foo/bar", "foo:bar"); 53 | test("foo/bar", "/foo:bar"); 54 | } 55 | 56 | #[test] 57 | fn test_invalid() { 58 | #[cfg(windows)] 59 | { 60 | use std::ffi::OsString; 61 | use std::os::windows::ffi::OsStringExt; 62 | 63 | let path = OsString::from_wide(&[0x66, 0x6F, 0xD800, 0x6F]); 64 | assert_eq!(&*path, Path::new(&path).localize_name()); 65 | } 66 | #[cfg(not(windows))] 67 | { 68 | use std::ffi::OsStr; 69 | use std::os::unix::ffi::OsStrExt; 70 | 71 | let path = OsStr::from_bytes(&[0x66, 0x6F, 0x80, 0x6F]); 72 | assert_eq!(path, Path::new(path).localize_name()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/rust.rs: -------------------------------------------------------------------------------- 1 | //! Tests copied and modified from The Rust Programming Language. 2 | //! 3 | //! Sources: 4 | //! - 5 | //! - 6 | //! 7 | //! Copyrights: 8 | //! - Copyrights in the Rust project are retained by their contributors. No 9 | //! copyright assignment is required to contribute to the Rust project. 10 | //! 11 | //! Some files include explicit copyright notices and/or license notices. 12 | //! For full authorship information, see the version control history or 13 | //! 14 | //! 15 | //! 16 | //! - Modifications copyright (c) 2020 dylni ()
17 | //! 18 | 19 | #[macro_use] 20 | mod common; 21 | 22 | #[test] 23 | fn test_simple() { 24 | if cfg!(windows) { 25 | test!(r"a\b\c", r"X:\ABC\a\b\c", SAME); 26 | test!(r"a/b\c", r"X:\ABC\a\b\c", SAME); 27 | test!(r"a/b\c\", r"X:\ABC\a\b\c\", SAME); 28 | test!(r"a/b\c/", r"X:\ABC\a\b\c\", SAME); 29 | test!(r"\\", r"X:\\", r"X:\"); 30 | test!(r"/", r"X:\", SAME); 31 | test!(r"//", r"X:\\", r"X:\"); 32 | 33 | test!(r"C:\a\b", SAME, SAME); 34 | test!(r"C:\", SAME, SAME); 35 | test!(r"C:\.", SAME, r"C:\"); 36 | test!(r"C:\..", SAME, r"C:\"); 37 | 38 | test!(r"\\server\share\a\b", SAME, SAME); 39 | test!(r"\\server\share\a\.\b", SAME, r"\\server\share\a\b"); 40 | test!(r"\\server\share\a\..\b", SAME, r"\\server\share\b"); 41 | test!(r"\\server\share\a\b\", SAME, SAME); 42 | 43 | test!(r"\\?\a\b", SAME, SAME); 44 | test!(r"\\?\a/\\b\", SAME, SAME); 45 | test!(r"\\?\a/\\b/", SAME, SAME); 46 | test!(r"\\?\a\b", SAME, SAME); 47 | } else { 48 | test!("/", SAME, SAME); 49 | test!("//", SAME, "/"); 50 | 51 | test!("/.", SAME, "/"); 52 | test!("/..", SAME, "/"); 53 | test!("/../../", SAME, "/"); 54 | 55 | if cfg!(target_os = "macos") { 56 | test!(".", "/tmp/.", "/private/tmp"); 57 | test!("..", "/tmp/..", "/private"); 58 | test!("/tmp", SAME, "/private/tmp"); 59 | test!("//tmp", SAME, "/private/tmp"); 60 | test!("../tmp/", "/tmp/../tmp/", "/private/tmp"); 61 | test!("../tmp/../tmp/../", "/tmp/../tmp/../tmp/../", "/private"); 62 | } else { 63 | test!(".", "/tmp/.", "/tmp"); 64 | test!("..", "/tmp/..", "/"); 65 | test!("/tmp", SAME, SAME); 66 | test!("//tmp", SAME, "/tmp"); 67 | test!("../tmp/", "/tmp/../tmp/", "/tmp"); 68 | test!("../tmp/../tmp/../", "/tmp/../tmp/../tmp/../", "/"); 69 | } 70 | } 71 | } 72 | 73 | #[cfg(windows)] 74 | #[test] 75 | fn test_complex() { 76 | use common::test_join; 77 | 78 | test_join(r"c:\", r"windows", r"c:\windows"); 79 | test_join(r"c:", r"windows", r"c:windows"); 80 | 81 | test_join(r"C:\a", r"C:\b.txt", r"C:\b.txt"); 82 | test_join(r"C:\a\b\c", "C:d", r"C:\a\b\c\d"); 83 | test_join(r"C:a\b\c", "C:d", r"C:a\b\c\d"); 84 | test_join(r"C:", r"a\b\c", r"C:a\b\c"); 85 | test_join(r"C:", r"..\a", r"C:..\a"); 86 | test_join(r"\\server\share\foo", "bar", r"\\server\share\foo\bar"); 87 | test_join(r"\\server\share\foo", "C:baz", "C:baz"); 88 | test_join(r"\\?\C:\a\b", r"C:c\d", r"C:c\d"); 89 | test_join(r"\\?\C:a\b", r"C:c\d", r"C:c\d"); 90 | test_join(r"\\?\C:\a\b", r"C:\c\d", r"C:\c\d"); 91 | test_join(r"\\?\foo\bar", "baz", r"\\?\foo\bar\baz"); 92 | test_join( 93 | r"\\?\UNC\server\share\foo", 94 | "bar", 95 | r"\\?\UNC\server\share\foo\bar", 96 | ); 97 | test_join(r"\\?\UNC\server\share", r"C:\a", r"C:\a"); 98 | test_join(r"\\?\UNC\server\share", "C:a", "C:a"); 99 | 100 | test_join(r"\\?\UNC\server", "foo", r"\\?\UNC\server\foo"); 101 | 102 | test_join(r"C:\a", r"\\?\UNC\server\share", r"\\?\UNC\server\share"); 103 | test_join(r"\\.\foo\bar", "baz", r"\\.\foo\bar\baz"); 104 | test_join(r"\\.\foo\bar", "C:a", "C:a"); 105 | test_join(r"\\.\foo", r"..\bar", r"\\.\foo\bar"); 106 | } 107 | -------------------------------------------------------------------------------- /tests/windows.rs: -------------------------------------------------------------------------------- 1 | // https://googleprojectzero.blogspot.com/2016/02/the-definitive-guide-on-win32-to-nt.html 2 | 3 | #![cfg(windows)] 4 | 5 | use std::env; 6 | use std::io; 7 | use std::path::Path; 8 | 9 | use normpath::PathExt; 10 | 11 | #[macro_use] 12 | mod common; 13 | 14 | #[test] 15 | fn test_drive_absolute() { 16 | test!(r"X:\ABC\DEF", SAME, SAME); 17 | test!(r"X:\", SAME, SAME); 18 | test!(r"X:\ABC\", SAME, SAME); 19 | test!(r"X:\ABC\DEF. .", SAME, r"X:\ABC\DEF"); 20 | test!(r"X:/ABC/DEF", SAME, r"X:\ABC\DEF"); 21 | test!(r"X:\ABC\..\XYZ", SAME, r"X:\XYZ"); 22 | test!(r"X:\ABC\..\..\..", SAME, r"X:\"); 23 | } 24 | 25 | #[test] 26 | fn test_drive_relative() { 27 | test!(r"X:DEF\GHI", r"X:\ABC\DEF\GHI", SAME); 28 | test!(r"X:", r"X:\ABC", SAME); 29 | test!(r"X:DEF. .", r"X:\ABC\DEF. .", r"X:\ABC\DEF"); 30 | test!(r"Y:", SAME, r"Y:\"); 31 | test!(r"Z:", SAME, r"Z:\"); 32 | test!(r"X:ABC\..\XYZ", r"X:\ABC\ABC\..\XYZ", r"X:\ABC\XYZ"); 33 | test!(r"X:ABC\..\..\..", r"X:\ABC\ABC\..\..\..", r"X:\"); 34 | } 35 | 36 | #[test] 37 | fn test_rooted() { 38 | test!(r"\ABC\DEF", r"X:\ABC\DEF", SAME); 39 | test!(r"\", r"X:\", SAME); 40 | test!(r"\ABC\DEF. .", r"X:\ABC\DEF. .", r"X:\ABC\DEF"); 41 | test!(r"/ABC/DEF", r"X:\ABC\DEF", SAME); 42 | test!(r"\ABC\..\XYZ", r"X:\ABC\..\XYZ", r"X:\XYZ"); 43 | test!(r"\ABC\..\..\..", r"X:\ABC\..\..\..", r"X:\"); 44 | } 45 | 46 | #[test] 47 | fn test_relative() { 48 | test!(r"XYZ\DEF", r"X:\ABC\XYZ\DEF", SAME); 49 | test!(r".", r"X:\ABC", SAME); 50 | test!(r"XYZ\DEF. .", r"X:\ABC\XYZ\DEF. .", r"X:\ABC\XYZ\DEF"); 51 | test!(r"XYZ/DEF", r"X:\ABC\XYZ\DEF", SAME); 52 | test!(r"..\XYZ", r"X:\XYZ", SAME); 53 | test!(r"XYZ\..\..\..", r"X:\ABC\XYZ\..\..\..", r"X:\"); 54 | } 55 | 56 | #[test] 57 | fn test_unc_absolute() { 58 | test!(r"\\server\share\ABC\DEF", SAME, SAME); 59 | test!(r"\\server\share", SAME, SAME); 60 | test!(r"\\server\share\ABC. .", SAME, r"\\server\share\ABC"); 61 | test!(r"//server/share/ABC/DEF", SAME, r"\\server\share\ABC\DEF"); 62 | test!(r"\\server\share\ABC\..\XYZ", SAME, r"\\server\share\XYZ"); 63 | test!(r"\\server\share\ABC\..\..\..", SAME, r"\\server\share"); 64 | 65 | assert_eq!( 66 | "partial UNC prefixes are invalid", 67 | Path::new(r"\\server") 68 | .normalize_virtually() 69 | .unwrap_err() 70 | .to_string(), 71 | ); 72 | } 73 | 74 | #[test] 75 | fn test_local_device() { 76 | test!(r"\\.\COM20", SAME, SAME); 77 | test!(r"\\.\pipe\mypipe", SAME, SAME); 78 | test!(r"\\.\X:\ABC\DEF. .", SAME, r"\\.\X:\ABC\DEF"); 79 | test!(r"\\.\X:/ABC/DEF", SAME, r"\\.\X:\ABC\DEF"); 80 | test!(r"\\.\X:\ABC\..\XYZ", SAME, r"\\.\X:\XYZ"); 81 | test!(r"\\.\X:\ABC\..\..\C:\", SAME, r"\\.\C:\"); 82 | test!(r"\\.\pipe\mypipe\..\notmine", SAME, r"\\.\pipe\notmine"); 83 | 84 | test!(r"COM1", r"X:\ABC\COM1", r"\\.\COM1"); 85 | test!(r"X:\COM1", SAME, r"\\.\COM1"); 86 | test!(r"X:COM1", r"X:\ABC\COM1", r"\\.\COM1"); 87 | test!(r"valid\COM1", r"X:\ABC\valid\COM1", r"\\.\COM1"); 88 | test!(r"X:\notvalid\COM1", SAME, r"\\.\COM1"); 89 | test!(r"X:\COM1.blah", SAME, r"\\.\COM1"); 90 | test!(r"X:\COM1:blah", SAME, r"\\.\COM1"); 91 | test!(r"X:\COM1 .blah", SAME, r"\\.\COM1"); 92 | test!(r"\\.\X:\COM1", SAME, SAME); 93 | test!(r"\\abc\xyz\COM1", SAME, SAME); 94 | } 95 | 96 | #[test] 97 | fn test_root_local_device() { 98 | test!(r"\\?\X:\ABC\DEF", SAME, SAME); 99 | test!(r"\\?\X:\", SAME, SAME); 100 | test!(r"\\?\X:", SAME, SAME); 101 | test!(r"\\?\X:\COM1", SAME, SAME); 102 | test!(r"\\?\X:\ABC\DEF. .", SAME, SAME); 103 | test!(r"\\?\X:/ABC/DEF", SAME, SAME); 104 | test!(r"\\?\X:\ABC\..\XYZ", SAME, SAME); 105 | test!(r"\\?\X:\ABC\..\..\..", SAME, SAME); 106 | 107 | // This prefix is not parsed by the standard library: 108 | // https://github.com/rust-lang/rust/issues/56030 109 | // 110 | // test!(r"\??\X:\ABC\DEF", SAME, SAME); 111 | // test!(r"\??\X:\", SAME, SAME); 112 | // test!(r"\??\X:", SAME, SAME); 113 | // test!(r"\??\X:\COM1", SAME, SAME); 114 | // test!(r"\??\X:\ABC\DEF. .", SAME, SAME); 115 | // test!(r"\??\X:/ABC/DEF", SAME, SAME); 116 | // test!(r"\??\X:\ABC\..\XYZ", SAME, SAME); 117 | // test!(r"\??\X:\ABC\..\..\..", SAME, SAME); 118 | } 119 | 120 | #[test] 121 | fn test_edge_cases() { 122 | test!(r"//?/X:/ABC/DEF", SAME, r"\\?\X:\ABC\DEF"); 123 | test!(r"//?/X:/", SAME, r"\\?\X:\"); 124 | test!(r"//?/X:", SAME, r"\\?\X:"); 125 | 126 | // This prefix is not parsed by the standard library: 127 | // https://github.com/rust-lang/rust/issues/56030 128 | // 129 | // test!(r"/??/X:/ABC/DEF", SAME, r"\??\X:\ABC\DEF"); 130 | // test!(r"/??/X:/", SAME, r"\??\X:\"); 131 | // test!(r"/??/X:", SAME, r"\??\X:"); 132 | } 133 | 134 | #[test] 135 | fn test_short() { 136 | #[track_caller] 137 | fn test(short_path: &str, long_path: &str) { 138 | let short_path = Path::new(short_path); 139 | let long_path = Path::new(long_path); 140 | common::assert_eq(long_path, short_path.expand()); 141 | common::assert_eq(short_path, long_path.shorten()); 142 | assert_ne!( 143 | long_path, 144 | short_path 145 | .normalize_virtually() 146 | .expect("failed to normalize existing path"), 147 | ); 148 | } 149 | 150 | test(r"C:\DOCUME~1", r"C:\Documents and Settings"); 151 | test(r"C:\PROGRA~1", r"C:\Program Files"); 152 | test(r"C:\PROGRA~2", r"C:\Program Files (x86)"); 153 | } 154 | 155 | #[test] 156 | fn test_existing() { 157 | #[track_caller] 158 | fn test(path: &str) { 159 | let path = Path::new(path); 160 | assert!(path.metadata().is_ok()); 161 | common::assert_eq(path, path.normalize()); 162 | common::assert_eq(path, path.expand()); 163 | } 164 | 165 | test(r"C:\Documents and Settings"); 166 | test(r"\\localhost\C$\Documents and Settings"); 167 | test(r"\\?\C:\Documents and Settings"); 168 | } 169 | 170 | // https://github.com/dylni/normpath/issues/5 171 | #[test] 172 | fn test_windows_bug() -> io::Result<()> { 173 | let initial_current_dir = env::current_dir()?; 174 | 175 | for current_dir in [r"C:\", r"C:\Users"] { 176 | let current_dir = Path::new(current_dir); 177 | env::set_current_dir(current_dir)?; 178 | common::assert_eq(current_dir, env::current_dir()); 179 | 180 | common::assert_eq(current_dir, Path::new(".").normalize()); 181 | } 182 | 183 | env::set_current_dir(initial_current_dir) 184 | } 185 | --------------------------------------------------------------------------------