diff --git a/Cargo.lock b/Cargo.lock index 7569261e00f7..7bc029517859 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1112,6 +1112,7 @@ dependencies = [ "serde", "serde_json", "sha2", + "sys-locale", "time 0.3.36", "tokio", "unic-langid", @@ -6000,6 +6001,15 @@ dependencies = [ "syn", ] +[[package]] +name = "sys-locale" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e801cf239ecd6ccd71f03d270d67dd53d13e90aab208bf4b8fe4ad957ea949b0" +dependencies = [ + "libc", +] + [[package]] name = "tabs" version = "0.1.0" diff --git a/supply-chain/audits.toml b/supply-chain/audits.toml index 0b80527423ba..5a144e3d1d7c 100644 --- a/supply-chain/audits.toml +++ b/supply-chain/audits.toml @@ -4434,6 +4434,12 @@ who = "Mike Hommey " criteria = "safe-to-deploy" delta = "0.13.0 -> 0.13.1" +[[audits.sys-locale]] +who = "Alex Franchuk " +criteria = "safe-to-deploy" +version = "0.3.1" +notes = "Succinct and easily-verified unsafe code." + [[audits.tempfile]] who = "Mike Hommey " criteria = "safe-to-deploy" diff --git a/third_party/rust/sys-locale/.cargo-checksum.json b/third_party/rust/sys-locale/.cargo-checksum.json new file mode 100644 index 000000000000..5a3e73d4f939 --- /dev/null +++ b/third_party/rust/sys-locale/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"CHANGELOG.md":"ab8e78c1df9e7015794686a41f83ca4733d928793258b189bcb85e6e5475263a","Cargo.lock":"e2e11385f11df2be63f65ebe8e5bbf2e6be2226adc05b88e0b123bebedfffaec","Cargo.toml":"2fb1b281dfca4b7f44c90fe691578a981a7fc8d36c7c903f6342f6ba6eb6b18d","LICENSE-APACHE":"c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4","LICENSE-MIT":"5cc390ab5e5e6507f467368cbab1297552d64bfd863f4b496c7928508bcfefd8","README.md":"9337fea35f911279642f245dd3ed329075e612105e8a45c3a5f7f34e38cd50df","examples/get_locale.rs":"3935c00fbc07d8ac07ddd1816bd9a73b70826cf8fd7a66daa974442d11f2b109","src/android.rs":"5ee2b203648b3b563131d89d87a647693b56a326aadb1cb95477c126ab605073","src/apple.rs":"6a382fd500e90d66d5633d2011940221e96e1b74f0ceb0f742115a0dce8e5c35","src/lib.rs":"08df773f06960d431dd1e9e901a7d253c7e1dc3802bdf57eafc84588b6528ae4","src/unix.rs":"dd5278a4f4191dd68a83f6d6ca7c1e52190044a9fd76bd63f3ed4618cb4cfdfe","src/wasm.rs":"a280cf369a7a6ba68e59c083668441d3beb0eae13fa1c46262deea363aa52f92","src/windows.rs":"58cf9bb2a42b95d96073e2222a9f3a773a2b40f4867eabff3373353b33eba40a","src/windows_sys.rs":"86e5943e0ceaafacca208ccc3ce61143792713a2dd6d2859a15221ab92d7df5f","tests/wasm_worker.rs":"ad7d7a8728676f6c594a74e17e369a8f67c1a5c40e54f68a85a96327cd9ea285"},"package":"e801cf239ecd6ccd71f03d270d67dd53d13e90aab208bf4b8fe4ad957ea949b0"} \ No newline at end of file diff --git a/third_party/rust/sys-locale/CHANGELOG.md b/third_party/rust/sys-locale/CHANGELOG.md new file mode 100644 index 000000000000..f1344888cfe9 --- /dev/null +++ b/third_party/rust/sys-locale/CHANGELOG.md @@ -0,0 +1,73 @@ +# sys-locale changelog + +Notable changes to this project will be documented in the [keep a changelog](https://keepachangelog.com/en/1.0.0/) format. + +## [Unreleased] + +## [0.3.1] - 2023-08-27 + +### Added +- Added support for getting a list of user locales in their preferred order via `get_locales`. + - Additional locales are currently supported on iOS, macOS, WASM, and Windows. Other platforms will + only return a single locale like `get_locale` does. + +### Changed +- Removed `windows-sys` dependency + +## [0.3.0] - 2023-04-04 + +### Changed +- The crate now only uses `wasm-bindgen` when targeting WebAssembly on the web. + Use the new `js` feature to target the web. + +### Fixed +- The crate now compiles for unsupported platforms. +- Cleaned up typos and grammar in README. + +# [0.2.4] - 2023-03-07 + +### Changed +- Removed dependency on the `winapi` crate in favor of `windows-sys`, following more of the wider ecosystem. + +## [0.2.3] - 2022-11-06 + +### Fixed +- Re-release 0.2.2 and correctly maintain `no_std` compatibility on Apple targets. + +## [0.2.2] - 2022-11-06 + +### Changed +- The Apple backend has been rewritten in pure Rust instead of Objective-C. + +### Fixed +- The locale returned on UNIX systems is now always a correctly formatted BCP-47 tag. + +## [0.2.1] - 2022-06-16 + +### Added + +- The crate now supports being used via WASM in a WebWorker environment. + +## [0.2.0] - 2022-03-01 + +### Fixed + +- Fixed a soundness issue on Linux and BSDs by querying the environment directly instead of using libc setlocale. The libc setlocale is not safe for use in a multi-threaded context. + +### Changed + +- No longer `no_std` on Linux and BSDs + +## [0.1.0] - 2021-05-13 + +Initial release + +[Unreleased]: https://github.com/1Password/sys-locale/compare/v0.3.1...HEAD +[0.1.0]: https://github.com/1Password/sys-locale/releases/tag/v0.1.0 +[0.2.0]: https://github.com/1Password/sys-locale/releases/tag/v0.2.0 +[0.2.1]: https://github.com/1Password/sys-locale/releases/tag/v0.2.1 +[0.2.2]: https://github.com/1Password/sys-locale/releases/tag/v0.2.2 +[0.2.3]: https://github.com/1Password/sys-locale/releases/tag/v0.2.3 +[0.2.4]: https://github.com/1Password/sys-locale/releases/tag/v0.2.4 +[0.3.0]: https://github.com/1Password/sys-locale/releases/tag/v0.3.0 +[0.3.1]: https://github.com/1Password/sys-locale/releases/tag/v0.3.1 diff --git a/third_party/rust/sys-locale/Cargo.lock b/third_party/rust/sys-locale/Cargo.lock new file mode 100644 index 000000000000..7b8ae92f76fd --- /dev/null +++ b/third_party/rust/sys-locale/Cargo.lock @@ -0,0 +1,207 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bumpalo" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "syn" +version = "2.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sys-locale" +version = "0.3.1" +dependencies = [ + "js-sys", + "libc", + "wasm-bindgen", + "wasm-bindgen-test", + "web-sys", +] + +[[package]] +name = "unicode-ident" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e6e302a7ea94f83a6d09e78e7dc7d9ca7b186bc2829c24a22d0753efd680671" +dependencies = [ + "console_error_panic_hook", + "js-sys", + "scoped-tls", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecb993dd8c836930ed130e020e77d9b2e65dd0fbab1b67c790b0f5d80b11a575" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "web-sys" +version = "0.3.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" +dependencies = [ + "js-sys", + "wasm-bindgen", +] diff --git a/third_party/rust/sys-locale/Cargo.toml b/third_party/rust/sys-locale/Cargo.toml new file mode 100644 index 000000000000..9f2948c0f8fd --- /dev/null +++ b/third_party/rust/sys-locale/Cargo.toml @@ -0,0 +1,57 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2018" +name = "sys-locale" +version = "0.3.1" +authors = ["1Password"] +description = "Small and lightweight library to obtain the active system locale" +readme = "README.md" +keywords = [ + "locale", + "i18n", + "localization", + "nostd", +] +license = "MIT OR Apache-2.0" +repository = "https://github.com/1Password/sys-locale" + +[features] +js = [ + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[target."cfg(all(target_family = \"wasm\", not(unix)))".dependencies.js-sys] +version = "0.3" +optional = true + +[target."cfg(all(target_family = \"wasm\", not(unix)))".dependencies.wasm-bindgen] +version = "0.2" +optional = true + +[target."cfg(all(target_family = \"wasm\", not(unix)))".dependencies.web-sys] +version = "0.3" +features = [ + "Window", + "WorkerGlobalScope", + "Navigator", + "WorkerNavigator", +] +optional = true + +[target."cfg(all(target_family = \"wasm\", not(unix)))".dev-dependencies.wasm-bindgen-test] +version = "0.3" + +[target."cfg(target_os = \"android\")".dependencies.libc] +version = "0.2" diff --git a/third_party/rust/sys-locale/LICENSE-APACHE b/third_party/rust/sys-locale/LICENSE-APACHE new file mode 100644 index 000000000000..261eeb9e9f8b --- /dev/null +++ b/third_party/rust/sys-locale/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/third_party/rust/sys-locale/LICENSE-MIT b/third_party/rust/sys-locale/LICENSE-MIT new file mode 100644 index 000000000000..340215c67ba4 --- /dev/null +++ b/third_party/rust/sys-locale/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 1Password + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third_party/rust/sys-locale/README.md b/third_party/rust/sys-locale/README.md new file mode 100644 index 000000000000..e7754435b033 --- /dev/null +++ b/third_party/rust/sys-locale/README.md @@ -0,0 +1,54 @@ +# sys-locale + +[![crates.io version](https://img.shields.io/crates/v/sys-locale.svg)](https://crates.io/crates/sys-locale) +[![crate documentation](https://docs.rs/sys-locale/badge.svg)](https://docs.rs/sys-locale) +![MSRV](https://img.shields.io/badge/rustc-1.48+-blue.svg) +[![crates.io downloads](https://img.shields.io/crates/d/sys-locale.svg)](https://crates.io/crates/sys-locale) +![CI](https://github.com/1Password/sys-locale/workflows/CI/badge.svg) + +A small and lightweight Rust library to get the current active locale on the system. + +`sys-locale` is small library to get the current locale set for the system or application with the relevant platform APIs. The library is also `no_std` compatible, relying only on `alloc`, except on Linux and BSD. + +Platform support currently includes: +- Android +- iOS +- macOS +- Linux, BSD, and other UNIX variations +- WebAssembly, for the following platforms: + - Inside of a web browser (via the `js` feature) + - Emscripten (via the `UNIX` backend) + Further support for other WASM targets is dependent on upstream + support in those target's runtimes and specifications. +- Windows + +```rust +use sys_locale::get_locale; + +let locale = get_locale().unwrap_or_else(|| String::from("en-US")); + +println!("The current locale is {}", locale); +``` + +## MSRV + +The Minimum Supported Rust Version is currently 1.48.0. This will be bumped to the latest stable version of Rust when needed. + +## Credits + +Made with ❤️ by the [1Password](https://1password.com/) team. + +#### License + + +Licensed under either of Apache License, Version +2.0 or MIT license at your option. + + +
+ + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this crate by you, as defined in the Apache-2.0 license, shall +be dual licensed as above, without any additional terms or conditions. + diff --git a/third_party/rust/sys-locale/examples/get_locale.rs b/third_party/rust/sys-locale/examples/get_locale.rs new file mode 100644 index 000000000000..5055c8f8d596 --- /dev/null +++ b/third_party/rust/sys-locale/examples/get_locale.rs @@ -0,0 +1,11 @@ +//! A small example to run on your computer to see what locale the library returns. +#![allow(unknown_lints)] +#![allow(clippy::uninlined_format_args)] + +use sys_locale::get_locale; + +fn main() { + let locale = get_locale().unwrap_or_else(|| String::from("en-US")); + + println!("The current locale is {}", locale); +} diff --git a/third_party/rust/sys-locale/src/android.rs b/third_party/rust/sys-locale/src/android.rs new file mode 100644 index 000000000000..416785bdedb1 --- /dev/null +++ b/third_party/rust/sys-locale/src/android.rs @@ -0,0 +1,75 @@ +use alloc::{string::String, vec}; +use core::convert::TryFrom; + +fn get_property(name: &'static [u8]) -> Option { + let mut value = vec![0u8; libc::PROP_VALUE_MAX as usize]; + // SAFETY: `name` is valid to read from and `value` is valid to write to. + let len = + unsafe { libc::__system_property_get(name.as_ptr().cast(), value.as_mut_ptr().cast()) }; + + usize::try_from(len) + .ok() + .filter(|n| *n != 0) + .and_then(move |n| { + // Remove excess bytes and the NUL terminator + value.resize(n, 0); + String::from_utf8(value).ok() + }) +} + +const LOCALE_KEY: &[u8] = b"persist.sys.locale\0"; +const PRODUCT_LOCALE_KEY: &[u8] = b"ro.product.locale\0"; + +const PRODUCT_LANGUAGE_KEY: &[u8] = b"ro.product.locale.language\0"; +const PRODUCT_REGION_KEY: &[u8] = b"ro.product.locale.region\0"; + +// Android 4.0 and below +const LANG_KEY: &[u8] = b"persist.sys.language\0"; +const COUNTRY_KEY: &[u8] = b"persist.sys.country\0"; +const LOCALEVAR_KEY: &[u8] = b"persist.sys.localevar\0"; + +// Ported from https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/jni/AndroidRuntime.cpp#431 +fn read_locale() -> Option { + if let Some(locale) = get_property(LOCALE_KEY) { + return Some(locale); + } + + // Android 4.0 and below + if let Some(mut language) = get_property(LANG_KEY) { + // The details of this functionality are not publically available, so this is just + // adapted "best effort" from the original code. + match get_property(COUNTRY_KEY) { + Some(country) => { + language.push('-'); + language.push_str(&country); + } + None => { + if let Some(variant) = get_property(LOCALEVAR_KEY) { + language.push('-'); + language.push_str(&variant); + } + } + }; + + return Some(language); + } + + if let Some(locale) = get_property(PRODUCT_LOCALE_KEY) { + return Some(locale); + } + + let product_language = get_property(PRODUCT_LANGUAGE_KEY); + let product_region = get_property(PRODUCT_REGION_KEY); + match (product_language, product_region) { + (Some(mut lang), Some(region)) => { + lang.push('-'); + lang.push_str(®ion); + Some(lang) + } + _ => None, + } +} + +pub(crate) fn get() -> impl Iterator { + read_locale().into_iter() +} diff --git a/third_party/rust/sys-locale/src/apple.rs b/third_party/rust/sys-locale/src/apple.rs new file mode 100644 index 000000000000..ed2b1167f0e5 --- /dev/null +++ b/third_party/rust/sys-locale/src/apple.rs @@ -0,0 +1,166 @@ +use alloc::{string::String, vec::Vec}; +use core::ffi::c_void; + +type CFIndex = isize; +type Boolean = u8; +type CFStringEncoding = u32; + +#[allow(non_upper_case_globals)] +const kCFStringEncodingUTF8: CFStringEncoding = 0x08000100; + +#[repr(C)] +#[derive(Clone, Copy)] +struct CFRange { + pub location: CFIndex, + pub length: CFIndex, +} + +type CFTypeRef = *const c_void; + +#[repr(C)] +struct __CFArray(c_void); +type CFArrayRef = *const __CFArray; + +#[repr(C)] +struct __CFString(c_void); +type CFStringRef = *const __CFString; + +// Most of these definitions come from `core-foundation-sys`, but we want this crate +// to be `no_std` and `core-foundation-sys` isn't currently. +#[link(name = "CoreFoundation", kind = "framework")] +extern "C" { + fn CFArrayGetCount(theArray: CFArrayRef) -> CFIndex; + fn CFArrayGetValueAtIndex(theArray: CFArrayRef, idx: CFIndex) -> *const c_void; + + fn CFStringGetLength(theString: CFStringRef) -> CFIndex; + fn CFStringGetBytes( + theString: CFStringRef, + range: CFRange, + encoding: CFStringEncoding, + lossByte: u8, + isExternalRepresentation: Boolean, + buffer: *mut u8, + maxBufLen: CFIndex, + usedBufLen: *mut CFIndex, + ) -> CFIndex; + + fn CFRelease(cf: CFTypeRef); + + fn CFLocaleCopyPreferredLanguages() -> CFArrayRef; +} + +pub(crate) fn get() -> impl Iterator { + let preferred_langs = get_languages(); + let mut idx = 0; + + #[allow(clippy::as_conversions)] + core::iter::from_fn(move || unsafe { + let (langs, num_langs) = preferred_langs.as_ref()?; + + // 0 to N-1 inclusive + if idx >= *num_langs { + return None; + } + + // SAFETY: The current index has been checked that its still within bounds of the array. + // XXX: We don't retain the strings because we know we have total ownership of the backing array. + let locale = CFArrayGetValueAtIndex(langs.0, idx) as CFStringRef; + idx += 1; + + // SAFETY: `locale` is a valid CFString pointer because the array will always contain a value. + let str_len = CFStringGetLength(locale); + + let range = CFRange { + location: 0, + length: str_len, + }; + + let mut capacity = 0; + // SAFETY: + // - `locale` is a valid CFString + // - The supplied range is within the length of the string. + // - `capacity` is writable. + // Passing NULL and `0` is correct for the buffer to get the + // encoded output length. + CFStringGetBytes( + locale, + range, + kCFStringEncodingUTF8, + 0, + false as Boolean, + core::ptr::null_mut(), + 0, + &mut capacity, + ); + + // Guard against a zero-sized allocation, if that were to somehow occur. + if capacity == 0 { + return None; + } + + // Note: This is the number of bytes (u8) that will be written to + // the buffer, not the number of codepoints they would contain. + let mut buffer = Vec::with_capacity(capacity as usize); + + // SAFETY: + // - `locale` is a valid CFString + // - The supplied range is within the length of the string. + // - `buffer` is writable and has sufficent capacity to receive the data. + // - `maxBufLen` is correctly based on `buffer`'s available capacity. + // - `out_len` is writable. + let mut out_len = 0; + CFStringGetBytes( + locale, + range, + kCFStringEncodingUTF8, + 0, + false as Boolean, + buffer.as_mut_ptr(), + capacity as CFIndex, + &mut out_len, + ); + + // Sanity check that both calls to `CFStringGetBytes` + // were equivalent. If they weren't, the system is doing + // something very wrong... + assert!(out_len <= capacity); + + // SAFETY: The system has written `out_len` elements, so they are + // initialized and inside the buffer's capacity bounds. + buffer.set_len(out_len as usize); + + // This should always contain UTF-8 since we told the system to + // write UTF-8 into the buffer, but the value is small enough that + // using `from_utf8_unchecked` isn't worthwhile. + String::from_utf8(buffer).ok() + }) +} + +fn get_languages() -> Option<(CFArray, CFIndex)> { + unsafe { + // SAFETY: This function is safe to call and has no invariants. Any value inside the + // array will be owned by us. + let langs = CFLocaleCopyPreferredLanguages(); + if !langs.is_null() { + let langs = CFArray(langs); + // SAFETY: The returned array is a valid CFArray object. + let count = CFArrayGetCount(langs.0); + if count != 0 { + Some((langs, count)) + } else { + None + } + } else { + None + } + } +} + +struct CFArray(CFArrayRef); + +impl Drop for CFArray { + fn drop(&mut self) { + // SAFETY: This wrapper contains a valid CFArray. + unsafe { CFRelease(self.0.cast()) } + } +} diff --git a/third_party/rust/sys-locale/src/lib.rs b/third_party/rust/sys-locale/src/lib.rs new file mode 100644 index 000000000000..15fd573fc735 --- /dev/null +++ b/third_party/rust/sys-locale/src/lib.rs @@ -0,0 +1,127 @@ +//! A library to safely and easily obtain the current locale on the system or for an application. +//! +//! This library currently supports the following platforms: +//! - Android +//! - iOS +//! - macOS +//! - Linux, BSD, and other UNIX variations +//! - WebAssembly on the web (via the `js` feature) +//! - Windows +#![cfg_attr( + any( + not(unix), + target_os = "macos", + target_os = "ios", + target_os = "android" + ), + no_std +)] +extern crate alloc; +use alloc::string::String; + +#[cfg(target_os = "android")] +mod android; +#[cfg(target_os = "android")] +use android as provider; + +#[cfg(any(target_os = "macos", target_os = "ios"))] +mod apple; +#[cfg(any(target_os = "macos", target_os = "ios"))] +use apple as provider; + +#[cfg(all( + unix, + not(any(target_os = "macos", target_os = "ios", target_os = "android")) +))] +mod unix; +#[cfg(all( + unix, + not(any(target_os = "macos", target_os = "ios", target_os = "android")) +))] +use unix as provider; + +#[cfg(all(target_family = "wasm", feature = "js", not(unix)))] +mod wasm; +#[cfg(all(target_family = "wasm", feature = "js", not(unix)))] +use wasm as provider; + +#[cfg(windows)] +mod windows; +#[cfg(windows)] +use windows as provider; + +#[cfg(not(any(unix, all(target_family = "wasm", feature = "js", not(unix)), windows)))] +mod provider { + pub fn get() -> impl Iterator { + core::iter::empty() + } +} + +/// Returns the active locale for the system or application. +/// +/// This may be equivalent to `get_locales().next()` (the first entry), +/// depending on the platform. +/// +/// # Returns +/// +/// Returns `Some(String)` with a BCP-47 language tag inside. If the locale +/// couldn't be obtained, `None` is returned instead. +/// +/// # Example +/// +/// ```no_run +/// use sys_locale::get_locale; +/// +/// let current_locale = get_locale().unwrap_or_else(|| String::from("en-US")); +/// +/// println!("The locale is {}", current_locale); +/// ``` +pub fn get_locale() -> Option { + get_locales().next() +} + +/// Returns the preferred locales for the system or application, in descending order of preference. +/// +/// # Returns +/// +/// Returns a `Vec` with any number of BCP-47 language tags inside. +/// If no locale preferences could be obtained, the vec will be empty. +/// +/// # Example +/// +/// ```no_run +/// use sys_locale::get_locales; +/// +/// let mut locales = get_locales(); +/// +/// println!("The most preferred locale is {}", locales.next().unwrap_or("en-US".to_string())); +/// println!("The least preferred locale is {}", locales.last().unwrap_or("en-US".to_string())); +/// ``` +pub fn get_locales() -> impl Iterator { + provider::get() +} + +#[cfg(test)] +mod tests { + use super::{get_locale, get_locales}; + extern crate std; + + #[cfg(all(target_family = "wasm", feature = "js", not(unix)))] + use wasm_bindgen_test::wasm_bindgen_test as test; + #[cfg(all(target_family = "wasm", feature = "js", not(unix)))] + wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + + #[test] + fn can_obtain_locale() { + assert!(get_locale().is_some(), "no locales were returned"); + let locales = get_locales(); + for (i, locale) in locales.enumerate() { + assert!(!locale.is_empty(), "locale string {} was empty", i); + assert!( + !locale.ends_with('\0'), + "locale {} contained trailing NUL", + i + ); + } + } +} diff --git a/third_party/rust/sys-locale/src/unix.rs b/third_party/rust/sys-locale/src/unix.rs new file mode 100644 index 000000000000..98349e6a9923 --- /dev/null +++ b/third_party/rust/sys-locale/src/unix.rs @@ -0,0 +1,101 @@ +#![allow(unknown_lints)] +use std::{env, ffi::OsStr}; + +const LC_ALL: &str = "LC_ALL"; +const LC_CTYPE: &str = "LC_CTYPE"; +const LANG: &str = "LANG"; + +/// Environment variable access abstraction to allow testing without +/// mutating env variables. +/// +/// Use [StdEnv] to query [std::env] +trait EnvAccess { + /// See also [std::env::var] + fn get(&self, key: impl AsRef) -> Option; +} + +/// Proxy to [std::env] +struct StdEnv; +impl EnvAccess for StdEnv { + fn get(&self, key: impl AsRef) -> Option { + env::var(key).ok() + } +} + +pub(crate) fn get() -> impl Iterator { + _get(&StdEnv).into_iter() +} + +fn _get(env: &impl EnvAccess) -> Option { + let code = env + .get(LC_ALL) + .or_else(|| env.get(LC_CTYPE)) + .or_else(|| env.get(LANG))?; + + parse_locale_code(&code) +} + +fn parse_locale_code(code: &str) -> Option { + // Some locales are returned with the char encoding too: `en_US.UTF-8` + // TODO: Once we bump MSRV >= 1.52, remove this allow and clean up + #[allow(clippy::manual_split_once)] + #[allow(clippy::needless_splitn)] + code.splitn(2, '.').next().map(|s| s.replace('_', "-")) +} + +#[cfg(test)] +mod tests { + use super::{parse_locale_code, EnvAccess, _get, LANG, LC_ALL, LC_CTYPE}; + use std::{ + collections::HashMap, + ffi::{OsStr, OsString}, + }; + + type MockEnv = HashMap; + impl EnvAccess for MockEnv { + fn get(&self, key: impl AsRef) -> Option { + self.get(key.as_ref()).cloned() + } + } + + const PARSE_LOCALE: &str = "fr-FR"; + const LANG_PARSE_LOCALE: &str = "fr_FR"; + + #[test] + fn parse_identifier() { + let identifier = "fr_FR.UTF-8"; + assert_eq!(parse_locale_code(identifier).as_deref(), Some(PARSE_LOCALE)); + } + + #[test] + fn parse_non_suffixed_identifier() { + assert_eq!( + parse_locale_code(PARSE_LOCALE).as_deref(), + Some(PARSE_LOCALE) + ); + + assert_eq!( + parse_locale_code(LANG_PARSE_LOCALE).as_deref(), + Some(PARSE_LOCALE) + ); + } + + #[test] + fn env_priority() { + let mut env = MockEnv::new(); + assert_eq!(_get(&env), None); + + // These locale names are technically allowed and some systems may still + // defined aliases such as these but the glibc sources mention that this + // should be considered deprecated + + env.insert(LANG.into(), "invalid".to_owned()); + assert_eq!(_get(&env).as_deref(), Some("invalid")); + + env.insert(LC_CTYPE.into(), "invalid-also".to_owned()); + assert_eq!(_get(&env).as_deref(), Some("invalid-also")); + + env.insert(LC_ALL.into(), "invalid-again".to_owned()); + assert_eq!(_get(&env).as_deref(), Some("invalid-again")); + } +} diff --git a/third_party/rust/sys-locale/src/wasm.rs b/third_party/rust/sys-locale/src/wasm.rs new file mode 100644 index 000000000000..0d266debb05d --- /dev/null +++ b/third_party/rust/sys-locale/src/wasm.rs @@ -0,0 +1,56 @@ +use alloc::string::String; + +use js_sys::{JsString, Object}; +use wasm_bindgen::{prelude::*, JsCast, JsValue}; + +#[derive(Clone)] +enum GlobalType { + Window(web_sys::Window), + Worker(web_sys::WorkerGlobalScope), +} + +/// Returns a handle to the global scope object. +/// +/// Simplified version of https://github.com/rustwasm/wasm-bindgen/blob/main/crates/js-sys/src/lib.rs, +/// which we can't use directly because it discards information about how it +/// retrieved the global. +fn global() -> GlobalType { + #[wasm_bindgen] + extern "C" { + type Global; + + #[wasm_bindgen(getter, catch, static_method_of = Global, js_class = window, js_name = window)] + fn get_window() -> Result; + + #[wasm_bindgen(getter, catch, static_method_of = Global, js_class = self, js_name = self)] + fn get_self() -> Result; + } + + if let Ok(window) = Global::get_window() { + GlobalType::Window( + window + .dyn_into::() + .expect("expected window to be an instance of Window"), + ) + } else if let Ok(worker) = Global::get_self() { + GlobalType::Worker( + worker + .dyn_into::() + .expect("expected self to be an instance of WorkerGlobalScope"), + ) + } else { + panic!("Unable to find global in this environment") + } +} + +pub(crate) fn get() -> impl Iterator { + let languages = match global() { + GlobalType::Window(window) => window.navigator().languages(), + GlobalType::Worker(worker) => worker.navigator().languages(), + }; + languages + .values() + .into_iter() + .flat_map(|v| v.and_then(|v| v.dyn_into::())) + .map(String::from) +} diff --git a/third_party/rust/sys-locale/src/windows.rs b/third_party/rust/sys-locale/src/windows.rs new file mode 100644 index 000000000000..1b74b7e2e568 --- /dev/null +++ b/third_party/rust/sys-locale/src/windows.rs @@ -0,0 +1,49 @@ +use alloc::{string::String, vec::Vec}; + +#[path = "./windows_sys.rs"] +mod windows_sys; +use windows_sys::{GetUserPreferredUILanguages, MUI_LANGUAGE_NAME, TRUE}; + +#[allow(clippy::as_conversions)] +pub(crate) fn get() -> impl Iterator { + let mut num_languages: u32 = 0; + let mut buffer_length: u32 = 0; + + // Calling this with null buffer will retrieve the required buffer length + let success = unsafe { + GetUserPreferredUILanguages( + MUI_LANGUAGE_NAME, + &mut num_languages, + core::ptr::null_mut(), + &mut buffer_length, + ) + } == TRUE; + if !success { + return Vec::new().into_iter(); + } + + let mut buffer = Vec::::new(); + buffer.resize(buffer_length as usize, 0); + + // Now that we have an appropriate buffer, we can query the names + let mut result = Vec::with_capacity(num_languages as usize); + let success = unsafe { + GetUserPreferredUILanguages( + MUI_LANGUAGE_NAME, + &mut num_languages, + buffer.as_mut_ptr(), + &mut buffer_length, + ) + } == TRUE; + + if success { + // The buffer contains names split by null char (0), and ends with two null chars (00) + for part in buffer.split(|i| *i == 0).filter(|p| !p.is_empty()) { + if let Ok(locale) = String::from_utf16(part) { + result.push(locale); + } + } + } + + result.into_iter() +} diff --git a/third_party/rust/sys-locale/src/windows_sys.rs b/third_party/rust/sys-locale/src/windows_sys.rs new file mode 100644 index 000000000000..ea2cf14fd764 --- /dev/null +++ b/third_party/rust/sys-locale/src/windows_sys.rs @@ -0,0 +1,22 @@ +// Bindings generated by `windows-bindgen` 0.51.1 + +#![allow( + non_snake_case, + non_upper_case_globals, + non_camel_case_types, + dead_code, + clippy::all +)] +#[link(name = "kernel32")] +extern "system" { + pub fn GetUserPreferredUILanguages( + dwflags: u32, + pulnumlanguages: *mut u32, + pwszlanguagesbuffer: PWSTR, + pcchlanguagesbuffer: *mut u32, + ) -> BOOL; +} +pub type BOOL = i32; +pub const MUI_LANGUAGE_NAME: u32 = 8u32; +pub type PWSTR = *mut u16; +pub const TRUE: BOOL = 1i32; diff --git a/third_party/rust/sys-locale/tests/wasm_worker.rs b/third_party/rust/sys-locale/tests/wasm_worker.rs new file mode 100644 index 000000000000..2840f5048d92 --- /dev/null +++ b/third_party/rust/sys-locale/tests/wasm_worker.rs @@ -0,0 +1,15 @@ +#![cfg(all(target_family = "wasm", feature = "js", not(unix)))] + +use wasm_bindgen_test::wasm_bindgen_test as test; +wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_worker); + +use sys_locale::{get_locale, get_locales}; + +#[test] +fn can_obtain_locale() { + assert!(get_locale().is_some(), "no locales were returned"); + let locales = get_locales(); + for (i, locale) in locales.enumerate() { + assert!(!locale.is_empty(), "locale string {} was empty", i); + } +} diff --git a/toolkit/crashreporter/CrashAnnotations.yaml b/toolkit/crashreporter/CrashAnnotations.yaml index f0fb229ae2b5..6176cd04b01b 100644 --- a/toolkit/crashreporter/CrashAnnotations.yaml +++ b/toolkit/crashreporter/CrashAnnotations.yaml @@ -713,6 +713,11 @@ ProductID: type: string ping: true +ProfileDirectory: + description: > + The directory of the active profile, if any. + type: string + ProfilerChildShutdownPhase: description: > When a child process shuts down, this describes if the profiler is running, diff --git a/toolkit/crashreporter/client/app/Cargo.toml b/toolkit/crashreporter/client/app/Cargo.toml index 4899535ebd7e..c22e785e2ca1 100644 --- a/toolkit/crashreporter/client/app/Cargo.toml +++ b/toolkit/crashreporter/client/app/Cargo.toml @@ -23,6 +23,7 @@ phf = "0.11" serde = { version = "1", features = ["derive"] } serde_json = "1" sha2 = "0.10" +sys-locale = "0.3" time = { version = "0.3", features = ["formatting", "macros", "serde"] } unic-langid = { version = "0.9.1" } uuid = { version = "1", features = ["v4", "serde"] } diff --git a/toolkit/crashreporter/client/app/src/config.rs b/toolkit/crashreporter/client/app/src/config.rs index 224b2ac52a73..b78a1f1fcc18 100644 --- a/toolkit/crashreporter/client/app/src/config.rs +++ b/toolkit/crashreporter/client/app/src/config.rs @@ -38,6 +38,8 @@ pub struct Config { pub events_dir: Option, /// The ping directory. pub ping_dir: Option, + /// The profile directory in use when the crash occurred. + pub profile_dir: Option, /// The dump file. /// /// If missing, an error dialog is displayed. @@ -97,7 +99,7 @@ impl Config { if self.restart_command.is_none() { self.restart_command = Some( - self.sibling_program_path(mozbuild::config::MOZ_APP_NAME) + self.installation_program_path(mozbuild::config::MOZ_APP_NAME) .into(), ) } @@ -188,9 +190,42 @@ impl Config { self.restart_command = None; } + self.load_profile_directory_from_extra(&extra); + Ok(extra) } + /// Load the profile directory from the extra file information. + /// + /// This also loads the following information that relies on the profile directory: + /// * localization strings from langpacks + fn load_profile_directory_from_extra(&mut self, extra: &serde_json::Value) { + let Some(profile_dir) = extra + .get("ProfileDirectory") + .and_then(|v| v.as_str()) + .map(PathBuf::from) + else { + return; + }; + if !profile_dir.exists() { + return; + } + + self.profile_dir = Some(profile_dir); + + // Update the localization, if applicable. + match lang::load_langpack( + self.profile_dir.as_deref().unwrap(), + extra.get("useragent_locale").and_then(|v| v.as_str()), + ) { + Ok(Some(strings)) => self.strings = Some(strings), + Ok(None) => (), + Err(e) => { + log::warn!("failed to read localization information from profile: {e:#}") + } + } + } + /// Get the path to the extra file. /// /// Returns None if no dump_file is set. @@ -366,27 +401,21 @@ impl Config { Ok(()) } - /// Get the path of a program that is a sibling of the crashreporter. - /// - /// On MacOS, this assumes that the crashreporter is its own application bundle within the main - /// program bundle. On other platforms this assumes siblings reside in the same directory as - /// the crashreporter. + /// Get the path of a program in the installation. /// /// The returned path isn't guaranteed to exist. // This method could be standalone rather than living in `Config`; it's here because it makes // sense that if it were to rely on anything, it would be the `Config` (and that may change in // the future). - pub fn sibling_program_path>(&self, program: N) -> PathBuf { + pub fn installation_program_path>(&self, program: N) -> PathBuf { let self_path = self_path(); let exe_extension = self_path.extension().unwrap_or_default(); + let mut p = program.as_ref().to_os_string(); if !exe_extension.is_empty() { - let mut p = program.as_ref().to_os_string(); p.push("."); p.push(exe_extension); - sibling_path(p) - } else { - sibling_path(program) } + installation_path().join(p) } cfg_if::cfg_if! { @@ -491,36 +520,43 @@ impl Config { } } -/// Get the path of a file that is a sibling of the crashreporter. +/// Get the path of resources for the Firefox installation containing the crashreporter. +pub fn installation_resource_path() -> &'static Path { + static PATH: Lazy = Lazy::new(|| { + if cfg!(all(not(mock), target_os = "macos")) { + installation_path().parent().unwrap().join("Resources") + } else { + installation_path().to_owned() + } + }); + &*PATH +} + +/// Get the path of the Firefox installation containing the crashreporter. /// /// On MacOS, this assumes that the crashreporter is its own application bundle within the main -/// program bundle. On other platforms this assumes siblings reside in the same directory as -/// the crashreporter. -/// -/// The returned path isn't guaranteed to exist. -pub fn sibling_path>(file: N) -> PathBuf { - // Expect shouldn't panic as we don't invoke the program without a parent directory. - let dir_path = self_path().parent().expect("program invoked based on PATH"); +/// program bundle. It returns the MacOS directory of the Firefox application bundle. +pub fn installation_path() -> &'static Path { + static PATH: Lazy<&'static Path> = Lazy::new(|| { + // Expect shouldn't panic as we don't invoke the program without a parent directory. + let dir_path = self_path().parent().expect("program invoked based on PATH"); - let mut path = dir_path.join(file.as_ref()); + if cfg!(all(not(mock), target_os = "macos")) { + // On macOS the crash reporter client is shipped as an application bundle contained + // within Firefox's main application bundle. So when it's invoked its current working + // directory looks like: + // Firefox.app/Contents/MacOS/crashreporter.app/Contents/MacOS/ - if !path.exists() && cfg!(all(not(mock), target_os = "macos")) { - // On macOS the crash reporter client is shipped as an application bundle contained - // within Firefox's main application bundle. So when it's invoked its current working - // directory looks like: - // Firefox.app/Contents/MacOS/crashreporter.app/Contents/MacOS/ - // The other applications we ship with Firefox are stored in the main bundle - // (Firefox.app/Contents/MacOS/) so we we need to go back three directories - // to reach them. - - // 3rd ancestor (the 0th element of ancestors has no paths removed) to remove - // `crashreporter.app/Contents/MacOS`. - if let Some(ancestor) = dir_path.ancestors().nth(3) { - path = ancestor.join(file.as_ref()); + // 3rd ancestor (the 0th element of ancestors has no paths removed) to remove + // `crashreporter.app/Contents/MacOS`. + if let Some(ancestor) = dir_path.ancestors().nth(3) { + return ancestor; + } } - } - path + dir_path + }); + &*PATH } fn self_path() -> &'static Path { diff --git a/toolkit/crashreporter/client/app/src/lang/langpack.rs b/toolkit/crashreporter/client/app/src/lang/langpack.rs new file mode 100644 index 000000000000..01e70e12be19 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/lang/langpack.rs @@ -0,0 +1,190 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use super::language_info::LanguageInfo; +use super::zip::{read_archive_file_as_string, read_zip, Archive}; +use crate::config::installation_resource_path; +use crate::std::path::{Path, PathBuf}; +use anyhow::Context; + +const LOCALE_PREF_KEY: &str = r#""intl.locale.requested""#; + +/// Use the profile language preferences to determine the localization to use. +pub fn read(profile_dir: &Path, locale: Option<&str>) -> anyhow::Result> { + let Some((identifier, langpack)) = select_langpack(profile_dir, locale)? else { + return Ok(None); + }; + + let mut zip = read_zip(&langpack)?; + + let ftl_definitions = read_archive_ftl_definitions(&mut zip, &identifier)?; + let ftl_branding = read_archive_ftl_branding(&mut zip, &identifier).unwrap_or_else(|e| { + log::warn!( + "failed to read branding for {identifier} from {}: {e:#}", + langpack.display() + ); + log::info!("using fallback branding info"); + LanguageInfo::default().ftl_branding + }); + + Ok(Some(LanguageInfo { + identifier, + ftl_definitions, + ftl_branding, + })) +} + +/// Select the langpack to use based on profile settings, the useragent locale, and the existence +/// of langpack files. +fn select_langpack( + profile_dir: &Path, + locale: Option<&str>, +) -> anyhow::Result> { + if let Some(path) = locale.and_then(|l| find_langpack_extension(profile_dir, l)) { + Ok(Some((locale.unwrap().to_string(), path))) + } else { + let Some(locales) = locales_from_prefs(profile_dir)? else { + return Ok(None); + }; + // Use the first language for which we can find a langpack extension. + locales + .iter() + .find_map(|lang| { + find_langpack_extension(profile_dir, lang).map(|path| (lang.to_string(), path)) + }) + .with_context(|| { + format!("couldn't locate langpack for requested locales ({locales:?})") + }) + .map(Some) + } +} + +fn locales_from_prefs(profile_dir: &Path) -> anyhow::Result>> { + let prefs = profile_dir.join("prefs.js"); + if !prefs.exists() { + log::debug!( + "no prefs.js exists in {}; not reading localization info", + profile_dir.display() + ); + return Ok(None); + } + + let prefs_contents = std::fs::read_to_string(&prefs) + .with_context(|| format!("while reading {}", prefs.display()))?; + + // Logic dictated by https://firefox-source-docs.mozilla.org/intl/locale.html#requested-locales + + let Some(langs) = parse_requested_locales(&prefs_contents) else { + // If the pref is unset, the installation locale should be used. + log::debug!( + "no locale pref set in {}; using installation locale", + prefs.display() + ); + return Ok(None); + }; + + Ok(Some(if langs.is_empty() { + // If the pref is an empty string, use the OS locale settings. + let os_locales: Vec = sys_locale::get_locales().collect(); + if os_locales.is_empty() { + log::debug!("no OS locales found"); + return Ok(None); + } + log::debug!( + "locale pref blank in {}; using OS locale settings ({os_locales:?})", + prefs.display() + ); + os_locales + } else { + langs.into_iter().map(|s| s.to_owned()).collect() + })) +} + +/// Parse the language pref (if any) from the prefs file. +/// +/// This finds the first string match for the regex `"intl.locale.requested"[ \t\n\r\f\v,]*"(.*)"`, +/// and splits and trims the first match group, returning the set of strings that results. +/// +/// For example: +/// ```rust +/// let input = r#""intl.locale.requested", "foo , bar,,""#; +/// let expected_output = Some(vec!["foo","bar"]); +/// assert_eq!(parse_requested_locales(input), output); +/// ``` +/// +/// This will parse the locales out of the user prefs file contents, which looks like +/// `user_pref("intl.locale.requested", "")`. +fn parse_requested_locales(prefs_content: &str) -> Option> { + let (_, s) = prefs_content.split_once(LOCALE_PREF_KEY)?; + let s = s.trim_start_matches(|c: char| c.is_whitespace() || c == ','); + let s = s.strip_prefix('"')?; + let (v, _) = s.split_once('"')?; + Some( + v.split(",") + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .collect(), + ) +} + +/// Find the langpack extension for the given locale. +fn find_langpack_extension(profile_dir: &Path, locale: &str) -> Option { + let path_tail = format!("extensions/langpack-{locale}@firefox.mozilla.org.xpi"); + + // Check profile extensions + let profile_path = profile_dir.join(&path_tail); + if profile_path.exists() { + return Some(profile_path); + } + + // Check installation extensions + let install_path = installation_resource_path().join(format!("browser/{}", &path_tail)); + if install_path.exists() { + return Some(install_path); + } + + None +} + +fn read_archive_ftl_definitions( + langpack: &mut Archive, + language_identifier: &str, +) -> anyhow::Result { + let path = format!("localization/{language_identifier}/crashreporter/crashreporter.ftl"); + read_archive_file_as_string(langpack, &path) +} + +fn read_archive_ftl_branding( + langpack: &mut Archive, + language_identifier: &str, +) -> anyhow::Result { + let path = format!("browser/localization/{language_identifier}/branding/brand.ftl"); + read_archive_file_as_string(langpack, &path) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn parse_locales_none() { + assert_eq!(parse_requested_locales(""), None); + } + + #[test] + fn parse_locales_empty() { + assert_eq!( + parse_requested_locales(r#"user_pref("intl.locale.requested","")"#), + Some(vec![]) + ); + } + + #[test] + fn parse_locales() { + assert_eq!( + parse_requested_locales(r#"user_pref("intl.locale.requested", "fr,en-US")"#), + Some(vec!["fr", "en-US"]) + ); + } +} diff --git a/toolkit/crashreporter/client/app/src/lang/mod.rs b/toolkit/crashreporter/client/app/src/lang/mod.rs index fba6364431c8..3ea77cbd5766 100644 --- a/toolkit/crashreporter/client/app/src/lang/mod.rs +++ b/toolkit/crashreporter/client/app/src/lang/mod.rs @@ -2,8 +2,10 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +mod langpack; mod language_info; mod omnijar; +mod zip; use fluent::{bundle::FluentBundle, FluentArgs, FluentResource}; use intl_memoizer::concurrent::IntlLangMemoizer; @@ -14,13 +16,23 @@ use std::collections::BTreeMap; /// Get the localized string bundle. pub fn load() -> anyhow::Result { - // TODO support langpacks, bug 1873210 omnijar::read().unwrap_or_else(|e| { log::warn!("failed to read localization data from the omnijar ({e:#}), falling back to bundled content"); Default::default() }).load_strings() } +/// Get a localized string bundle from the configured locale langpack, if any. +pub fn load_langpack( + profile_dir: &crate::std::path::Path, + locale: Option<&str>, +) -> anyhow::Result> { + langpack::read(profile_dir, locale).and_then(|r| match r { + Some(language_info) => language_info.load_strings().map(Some), + None => Ok(None), + }) +} + /// A bundle of localized strings. pub struct LangStrings { bundle: FluentBundle, diff --git a/toolkit/crashreporter/client/app/src/lang/omnijar.rs b/toolkit/crashreporter/client/app/src/lang/omnijar.rs index 944eb800d1d6..bff8cd300588 100644 --- a/toolkit/crashreporter/client/app/src/lang/omnijar.rs +++ b/toolkit/crashreporter/client/app/src/lang/omnijar.rs @@ -3,27 +3,19 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use super::language_info::LanguageInfo; -use crate::config::sibling_path; -use crate::std::{ - fs::File, - io::{BufRead, BufReader, Read}, - path::Path, -}; +use super::zip::{read_archive_file_as_string, read_zip}; +use crate::config::installation_resource_path; +use crate::std::io::{BufRead, BufReader}; use anyhow::Context; use once_cell::unsync::Lazy; -use zip::read::{ArchiveOffset, Config as ZipConfig, ZipArchive}; /// Read the appropriate localization fluent definitions from the omnijar files. /// /// Returns language information if found in adjacent omnijar files. pub fn read() -> anyhow::Result { - let mut path = sibling_path(if cfg!(target_os = "macos") { - "../Resources/omni.ja" - } else { - "omni.ja" - }); + let mut path = installation_resource_path().join("omni.ja"); - let mut zip = read_omnijar_file(&path)?; + let mut zip = read_zip(&path)?; let buf = BufReader::new( zip.by_name("res/multilocale.txt") .context("failed to read multilocale file in zip archive")?, @@ -53,7 +45,7 @@ pub fn read() -> anyhow::Result { path.push("browser"); path.push("omni.ja"); - let mut browser_omnijar = Lazy::new(|| match read_omnijar_file(&path) { + let mut browser_omnijar = Lazy::new(|| match read_zip(&path) { Err(e) => { log::debug!("no browser omnijar found at {}: {e:#}", path.display()); None @@ -91,29 +83,3 @@ pub fn read() -> anyhow::Result { ftl_branding, }) } - -/// Read a file from the given zip archive (omnijar) as a string. -fn read_archive_file_as_string( - archive: &mut ZipArchive, - path: &str, -) -> anyhow::Result { - let mut file = archive - .by_name(path) - .with_context(|| format!("failed to locate {path} in archive"))?; - let mut data = String::new(); - file.read_to_string(&mut data) - .with_context(|| format!("failed to read {path} from archive"))?; - Ok(data) -} - -fn read_omnijar_file(path: &Path) -> anyhow::Result> { - ZipArchive::with_config( - ZipConfig { - // The archive starts at the beginning of the file (it's a standard zip file, no - // prefix data). - archive_offset: ArchiveOffset::Known(0), - }, - File::open(&path).with_context(|| format!("failed to open {}", path.display()))?, - ) - .with_context(|| format!("failed to read zip archive in {}", path.display())) -} diff --git a/toolkit/crashreporter/client/app/src/lang/zip.rs b/toolkit/crashreporter/client/app/src/lang/zip.rs new file mode 100644 index 000000000000..de57b199efc5 --- /dev/null +++ b/toolkit/crashreporter/client/app/src/lang/zip.rs @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +//! Zip utilities related to localization packaging. + +use crate::std::{fs::File, io::Read, path::Path}; +use anyhow::{Context, Result}; +use zip::read::{ArchiveOffset, Config as ZipConfig, ZipArchive}; + +/// A firefox archive file. +pub type Archive = ZipArchive; + +/// Read a zip file. +pub fn read_zip(path: &Path) -> Result { + ZipArchive::with_config( + ZipConfig { + // The archive starts at the beginning of the file (it's a standard zip file, no + // prefix data). + // + // Without this explicit offset, the default behavior will not work for omnijar files + // (which have the central directory at the beginning of the file, rather than just + // before the end of central directory maker). + archive_offset: ArchiveOffset::Known(0), + }, + File::open(&path).with_context(|| format!("failed to open {}", path.display()))?, + ) + .with_context(|| format!("failed to read zip archive in {}", path.display())) +} + +/// Read a file from the given zip archive as a string. +pub fn read_archive_file_as_string(archive: &mut Archive, path: &str) -> anyhow::Result { + let mut file = archive + .by_name(path) + .with_context(|| format!("failed to locate {path} in archive"))?; + let mut data = String::new(); + file.read_to_string(&mut data) + .with_context(|| format!("failed to read {path} from archive"))?; + Ok(data) +} diff --git a/toolkit/crashreporter/client/app/src/logic.rs b/toolkit/crashreporter/client/app/src/logic.rs index 8836e21ebbfc..4263a1ca22e2 100644 --- a/toolkit/crashreporter/client/app/src/logic.rs +++ b/toolkit/crashreporter/client/app/src/logic.rs @@ -117,7 +117,7 @@ impl ReportCrash { extra: &self.extra, ping_dir: self.config.ping_dir.as_deref(), minidump_hash, - pingsender_path: self.config.sibling_program_path("pingsender").as_ref(), + pingsender_path: self.config.installation_program_path("pingsender").as_ref(), } .send() } @@ -127,6 +127,7 @@ impl ReportCrash { fn sanitize_extra(&mut self) { if let Some(map) = self.extra.as_object_mut() { // Remove these entries, they don't need to be sent. + map.remove("ProfileDirectory"); map.remove("ServerURL"); map.remove("StackTraces"); } diff --git a/toolkit/crashreporter/client/app/src/main.rs b/toolkit/crashreporter/client/app/src/main.rs index 65ea2b2841b0..b1053c95bef5 100644 --- a/toolkit/crashreporter/client/app/src/main.rs +++ b/toolkit/crashreporter/client/app/src/main.rs @@ -214,7 +214,7 @@ fn try_run(config: &mut Arc) -> anyhow::Result { } else { // Run minidump-analyzer to gather stack traces. { - let analyzer_path = config.sibling_program_path("minidump-analyzer"); + let analyzer_path = config.installation_program_path("minidump-analyzer"); let mut cmd = crate::process::background_command(&analyzer_path); if config.dump_all_threads { cmd.arg("--full"); diff --git a/toolkit/crashreporter/client/app/src/std/path.rs b/toolkit/crashreporter/client/app/src/std/path.rs index a4a91a19ba2d..52433e1a1632 100644 --- a/toolkit/crashreporter/client/app/src/std/path.rs +++ b/toolkit/crashreporter/client/app/src/std/path.rs @@ -7,7 +7,7 @@ pub use std::path::*; use super::mock::MockKey; -use std::ffi::OsStr; +use std::ffi::{OsStr, OsString}; macro_rules! delegate { ( fn $name:ident (&self $(, $arg:ident : $argty:ty )* ) -> $ret:ty ) => { @@ -27,6 +27,13 @@ impl AsRef for Path { } } +impl ToOwned for Path { + type Owned = PathBuf; + fn to_owned(&self) -> Self::Owned { + PathBuf(self.0.to_owned()) + } +} + impl AsRef for Path { fn as_ref(&self) -> &OsStr { self.0.as_ref() @@ -51,6 +58,12 @@ impl AsRef for &OsStr { } } +impl AsRef for OsString { + fn as_ref(&self) -> &Path { + Path::from_path(self.as_ref()) + } +} + impl Path { fn from_path(path: &std::path::Path) -> &Self { // # Safety @@ -133,6 +146,12 @@ impl std::ops::Deref for PathBuf { } } +impl std::borrow::Borrow for PathBuf { + fn borrow(&self) -> &Path { + Path::from_path(self.0.as_ref()) + } +} + impl AsRef for PathBuf { fn as_ref(&self) -> &Path { Path::from_path(self.0.as_ref()) diff --git a/toolkit/crashreporter/nsExceptionHandler.cpp b/toolkit/crashreporter/nsExceptionHandler.cpp index be6106555ba8..3e1b4707e76a 100644 --- a/toolkit/crashreporter/nsExceptionHandler.cpp +++ b/toolkit/crashreporter/nsExceptionHandler.cpp @@ -2852,6 +2852,13 @@ static void SetCrashEventsDir(nsIFile* aDir) { } void SetProfileDirectory(nsIFile* aDir) { + // Record the profile directory for use by the crash reporter client. + { + nsAutoString path; + aDir->GetPath(path); + RecordAnnotationNSString(Annotation::ProfileDirectory, path); + } + nsCOMPtr dir; aDir->Clone(getter_AddRefs(dir));