Merge branch 'master' into conditional-style

This commit is contained in:
Filip Bachul 2022-04-26 21:36:51 +02:00
commit 54fa8a071c
18 changed files with 262 additions and 156 deletions

View File

@ -12,7 +12,7 @@
"disabled": false,
"expiration_symbol": "X",
"force_display": false,
"format": "on [$symbol($profile )(\\($region\\) )(\\[$duration\\])]($style)",
"format": "on [$symbol($profile )(\\($region\\) )(\\[$duration\\] )]($style)",
"profile_aliases": {},
"region_aliases": {},
"style": "bold yellow",
@ -468,7 +468,7 @@
"default": {
"always_show_remote": false,
"disabled": false,
"format": "on [$symbol$branch]($style)(:[$remote]($style)) ",
"format": "on [$symbol$branch(:$remote_branch)]($style) ",
"ignore_branches": [],
"only_attached": false,
"style": "bold purple",
@ -1486,7 +1486,7 @@
"properties": {
"format": {
"description": "The format for the module.",
"default": "on [$symbol($profile )(\\($region\\) )(\\[$duration\\])]($style)",
"default": "on [$symbol($profile )(\\($region\\) )(\\[$duration\\] )]($style)",
"type": "string"
},
"symbol": {
@ -2618,7 +2618,7 @@
"type": "object",
"properties": {
"format": {
"default": "on [$symbol$branch]($style)(:[$remote]($style)) ",
"default": "on [$symbol$branch(:$remote_branch)]($style) ",
"type": "string"
},
"symbol": {

View File

@ -284,7 +284,7 @@ jobs:
continue-on-error: true
steps:
- name: Merge | Merge Crowdin PR
run: gh pr merge --squash i18n_master
run: gh pr merge i18n_master --squash --repo=starship/starship
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@ -1,5 +1,23 @@
# Changelog
### [1.6.3](https://github.com/starship/starship/compare/v1.6.2...v1.6.3) (2022-04-26)
### Bug Fixes
* **git_branch:** correct variable name for remote branch ([#3897](https://github.com/starship/starship/issues/3897)) ([bd7957f](https://github.com/starship/starship/commit/bd7957f01c7fa2b14f068e4130f1aedea61f4a76))
* **schema:** move config-schema into docs folder ([#3878](https://github.com/starship/starship/issues/3878)) ([094f982](https://github.com/starship/starship/commit/094f982df184eecd85ea2832b3bf638629118c10))
### Performance Improvements
* **package:** only try to read files that exist ([#3904](https://github.com/starship/starship/issues/3904)) ([2a650bf](https://github.com/starship/starship/commit/2a650bfd140d561f955705cae124fb254ec549a1))
### Reverts
* **schema:** move config-schema back into .github folder ([#3886](https://github.com/starship/starship/issues/3886)) ([9b2ce42](https://github.com/starship/starship/commit/9b2ce4240c602df368f966996d870ef9197e65ac))
### [1.6.2](https://github.com/starship/starship/compare/v1.6.1...v1.6.2) (2022-04-15)

48
Cargo.lock generated
View File

@ -272,9 +272,9 @@ dependencies = [
[[package]]
name = "clap"
version = "3.1.10"
version = "3.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3124f3f75ce09e22d1410043e1e24f2ecc44fad3afe4f08408f1f7663d68da2b"
checksum = "7c167e37342afc5f33fd87bbc870cedd020d2a6dffa05d45ccd9241fbdd146db"
dependencies = [
"atty",
"bitflags",
@ -1055,6 +1055,17 @@ dependencies = [
"memoffset",
]
[[package]]
name = "nix"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f17df307904acd05aa8e32e97bb20f2a0df1728bbc2d771ae8f9a90463441e9"
dependencies = [
"bitflags",
"cfg-if 1.0.0",
"libc",
]
[[package]]
name = "nom"
version = "5.1.2"
@ -1898,7 +1909,7 @@ dependencies = [
[[package]]
name = "starship"
version = "1.6.2"
version = "1.6.3"
dependencies = [
"ansi_term",
"byte-unit",
@ -1914,7 +1925,7 @@ dependencies = [
"local_ipaddress",
"log",
"mockall",
"nix",
"nix 0.24.1",
"notify-rust",
"once_cell",
"open",
@ -1947,7 +1958,7 @@ dependencies = [
"urlencoding",
"versions",
"which",
"winapi",
"windows 0.35.0",
"winres",
"yaml-rust",
]
@ -1963,7 +1974,7 @@ dependencies = [
"lazycell",
"libc",
"mach",
"nix",
"nix 0.23.1",
"num-traits",
"uom",
"winapi",
@ -2310,9 +2321,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "versions"
version = "4.0.0"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5276c151793dde1cc57e08123f36f96e662a9f2532060c677612bf0e2c604d4"
checksum = "ee97e1d97bd593fb513912a07691b742361b3dd64ad56f2c694ea2dbfe0665d3"
dependencies = [
"itertools",
"nom 7.1.1",
@ -2320,9 +2331,9 @@ dependencies = [
[[package]]
name = "vtparse"
version = "0.6.0"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f41c9314c4dde1f43dd0c46c67bb5ae73850ce11eebaf7d8b912e178bda5401"
checksum = "36ce903972602c84dd48f488cdce39edcba03a93b7ca67b146ae862568f48c5c"
dependencies = [
"utf8parse",
]
@ -2408,6 +2419,19 @@ dependencies = [
"windows_x86_64_msvc 0.24.0",
]
[[package]]
name = "windows"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08746b4b7ac95f708b3cccceb97b7f9a21a8916dd47fc99b0e6aaf7208f26fd7"
dependencies = [
"windows_aarch64_msvc",
"windows_i686_gnu 0.35.0",
"windows_i686_msvc 0.35.0",
"windows_x86_64_gnu 0.35.0",
"windows_x86_64_msvc 0.35.0",
]
[[package]]
name = "windows-sys"
version = "0.35.0"
@ -2491,7 +2515,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "007a0353840b23e0c6dc73e5b962ff58ed7f6bc9ceff3ce7fe6fbad8d496edf4"
dependencies = [
"strum",
"windows",
"windows 0.24.0",
"xml-rs",
]
@ -2533,7 +2557,7 @@ dependencies = [
"futures-util",
"hex",
"lazy_static",
"nix",
"nix 0.23.1",
"once_cell",
"ordered-stream",
"rand 0.8.5",

View File

@ -1,6 +1,6 @@
[package]
name = "starship"
version = "1.6.2"
version = "1.6.3"
authors = ["Starship Contributors"]
build = "build.rs"
categories = ["command-line-utilities"]
@ -39,7 +39,7 @@ notify = ["notify-rust"]
ansi_term = "0.12.1"
byte-unit = "4.0.14"
chrono = "0.4.19"
clap = { version = "3.1.10", features = ["derive", "cargo", "unicode"] }
clap = { version = "3.1.12", features = ["derive", "cargo", "unicode"] }
clap_complete = "3.1.2"
dirs-next = "2.0.0"
dunce = "1.0.2"
@ -78,7 +78,7 @@ toml_edit = "0.14.2"
unicode-segmentation = "1.9.0"
unicode-width = "0.1.9"
urlencoding = "2.1.0"
versions = "4.0.0"
versions = "4.1.0"
which = "4.2.5"
yaml-rust = "0.4.5"
@ -92,11 +92,20 @@ optional = true
features = ["preserve_order", "indexmap"]
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.9", features = ["winuser", "securitybaseapi", "processthreadsapi", "handleapi", "impl-default"] }
deelevate = "0.2.0"
[target.'cfg(windows)'.dependencies.windows]
version = "0.35.0"
features = [
"Win32_Foundation",
"Win32_UI_Shell",
"Win32_Security",
"Win32_System_Threading",
"Win32_Storage_FileSystem",
]
[target.'cfg(not(windows))'.dependencies]
nix = "0.23.1"
nix = { version = "0.24.1", default-features = false, features = ["feature", "fs", "user"] }
[build-dependencies]
shadow-rs = "0.11.0"

View File

@ -111,7 +111,9 @@ description: Starship is the minimal, blazing fast, and extremely customizable p
#### Elvish
::: warning
Only elvish v0.18 or higher is supported.
:::
Add the following to the end of `~/.elvish/rc.elv`:
@ -135,9 +137,12 @@ description: Starship is the minimal, blazing fast, and extremely customizable p
#### Nushell
::: warning
This will change in the future.
Only Nushell v0.60+ is supported.
:::
Run the following:
```sh
mkdir ~/.cache/starship

View File

@ -306,16 +306,16 @@ date is read from the `AWSUME_EXPIRATION` env var.
### Options
| Option | Default | Description |
| ------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `format` | `'on [$symbol($profile )(\($region\) )(\[$duration\])]($style)'` | The format for the module. |
| `symbol` | `"☁️ "` | The symbol used before displaying the current AWS profile. |
| `region_aliases` | | Table of region aliases to display in addition to the AWS name. |
| `profile_aliases` | | Table of profile aliases to display in addition to the AWS name. |
| `style` | `"bold yellow"` | The style for the module. |
| `expiration_symbol` | `X` | The symbol displayed when the temporary credentials have expired. |
| `disabled` | `false` | Disables the `AWS` module. |
| `force_display` | `false` | If `true` displays info even if `credentials`, `credential_process` or `sso_start_url` have not been setup. |
| Option | Default | Description |
| ------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `format` | `'on [$symbol($profile )(\($region\) )(\[$duration\] )]($style)'` | The format for the module. |
| `symbol` | `"☁️ "` | The symbol used before displaying the current AWS profile. |
| `region_aliases` | | Table of region aliases to display in addition to the AWS name. |
| `profile_aliases` | | Table of profile aliases to display in addition to the AWS name. |
| `style` | `"bold yellow"` | The style for the module. |
| `expiration_symbol` | `X` | The symbol displayed when the temporary credentials have expired. |
| `disabled` | `false` | Disables the `AWS` module. |
| `force_display` | `false` | If `true` displays info even if `credentials`, `credential_process` or `sso_start_url` have not been setup. |
### Variables
@ -1211,6 +1211,7 @@ The module will be shown only if any of the following conditions are met:
- The `variable` configuration option is not defined, but the `default` configuration option is
::: tip
Multiple environmental variables can be displayed by using a `.`. (see example)
If the `variable` configuration option is not set, the module will display value of variable under the name of text after the `.` character.
@ -1418,17 +1419,17 @@ The `git_branch` module shows the active branch of the repo in your current dire
### Options
| Option | Default | Description |
| -------------------- | -------------------------------- | ---------------------------------------------------------------------------------------- |
| `always_show_remote` | `false` | Shows the remote tracking branch name, even if it is equal to the local branch name. |
| `format` | `"on [$symbol$branch]($style) "` | The format for the module. Use `"$branch"` to refer to the current branch name. |
| `symbol` | `" "` | A format string representing the symbol of git branch. |
| `style` | `"bold purple"` | The style for the module. |
| `truncation_length` | `2^63 - 1` | Truncates a git branch to `N` graphemes. |
| `truncation_symbol` | `"…"` | The symbol used to indicate a branch name was truncated. You can use `""` for no symbol. |
| `only_attached` | `false` | Only show the branch name when not in a detached `HEAD` state. |
| `ignore_branches` | `[]` | A list of names to avoid displaying. Useful for "master" or "main". |
| `disabled` | `false` | Disables the `git_branch` module. |
| Option | Default | Description |
| -------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| `always_show_remote` | `false` | Shows the remote tracking branch name, even if it is equal to the local branch name. |
| `format` | `"on [$symbol$branch(:$remote_branch)]($style) "` | The format for the module. Use `"$branch"` to refer to the current branch name. |
| `symbol` | `" "` | A format string representing the symbol of git branch. |
| `style` | `"bold purple"` | The style for the module. |
| `truncation_length` | `2^63 - 1` | Truncates a git branch to `N` graphemes. |
| `truncation_symbol` | `"…"` | The symbol used to indicate a branch name was truncated. You can use `""` for no symbol. |
| `only_attached` | `false` | Only show the branch name when not in a detached `HEAD` state. |
| `ignore_branches` | `[]` | A list of names to avoid displaying. Useful for "master" or "main". |
| `disabled` | `false` | Disables the `git_branch` module. |
### Variables

View File

@ -50,7 +50,7 @@ pub struct AwsConfig<'a> {
impl<'a> Default for AwsConfig<'a> {
fn default() -> Self {
AwsConfig {
format: "on [$symbol($profile )(\\($region\\) )(\\[$duration\\])]($style)",
format: "on [$symbol($profile )(\\($region\\) )(\\[$duration\\] )]($style)",
symbol: "☁️ ",
style: "bold yellow",
disabled: false,

View File

@ -18,7 +18,7 @@ pub struct GitBranchConfig<'a> {
impl<'a> Default for GitBranchConfig<'a> {
fn default() -> Self {
GitBranchConfig {
format: "on [$symbol$branch]($style)(:[$remote]($style)) ",
format: "on [$symbol$branch(:$remote_branch)]($style) ",
symbol: "",
style: "bold purple",
truncation_length: std::i64::MAX,

View File

@ -1,7 +1,7 @@
use crate::config::{ModuleConfig, StarshipConfig};
use crate::configs::StarshipRootConfig;
use crate::module::Module;
use crate::utils::{create_command, exec_timeout, CommandOutput};
use crate::utils::{create_command, exec_timeout, read_file, CommandOutput};
use crate::modules;
use crate::utils::{self, home_dir};
@ -342,6 +342,18 @@ impl<'a> Context<'a> {
.iter()
.find_map(|attempt| self.exec_cmd(attempt[0], &attempt[1..]))
}
/// Returns the string contents of a file from the current working directory
pub fn read_file_from_pwd(&self, file_name: &str) -> Option<String> {
if !self.try_begin_scan()?.set_files(&[file_name]).is_match() {
log::debug!(
"Not attempting to read {file_name} because, it was not found during scan."
);
return None;
}
read_file(self.current_dir.join(file_name)).ok()
}
}
#[derive(Debug)]

View File

@ -633,7 +633,7 @@ credential_process = /opt/bin/awscreds-retriever
"on {}",
Color::Yellow
.bold()
.paint("☁️ astronauts (ap-northeast-2) [30m]")
.paint("☁️ astronauts (ap-northeast-2) [30m] ")
));
assert_eq!(expected, actual);
@ -679,7 +679,7 @@ expiration={}
// on shared runners may delay it. Allow for up to 2 seconds of delay.
let possible_values = ["30m", "29m59s", "29m58s"];
let possible_values = possible_values.map(|duration| {
let segment_colored = format!("☁️ astronauts (ap-northeast-2) [{}]", duration);
let segment_colored = format!("☁️ astronauts (ap-northeast-2) [{}] ", duration);
Some(format!(
"on {}",
Color::Yellow.bold().paint(segment_colored)
@ -736,7 +736,7 @@ expiration={}
"on {}",
Color::Yellow
.bold()
.paint(format!("☁️ astronauts (ap-northeast-2) [{}]", symbol))
.paint(format!("☁️ astronauts (ap-northeast-2) [{}] ", symbol))
));
assert_eq!(expected, actual);

View File

@ -396,6 +396,42 @@ mod tests {
repo_dir.close()
}
#[test]
fn test_remote() -> io::Result<()> {
let remote_dir = fixture_repo(FixtureProvider::Git)?;
let repo_dir = fixture_repo(FixtureProvider::Git)?;
create_command("git")?
.args(&["checkout", "-b", "test_branch"])
.current_dir(repo_dir.path())
.output()?;
create_command("git")?
.args(&["remote", "add", "--fetch", "remote_repo"])
.arg(remote_dir.path())
.current_dir(repo_dir.path())
.output()?;
create_command("git")?
.args(&["branch", "--set-upstream-to", "remote_repo/master"])
.current_dir(repo_dir.path())
.output()?;
let actual = ModuleRenderer::new("git_branch")
.path(&repo_dir.path())
.config(toml::toml! {
[git_branch]
format = "$branch(:$remote_name/$remote_branch)"
})
.collect();
let expected = Some("test_branch:remote_repo/master");
assert_eq!(expected, actual.as_deref());
repo_dir.close()?;
remote_dir.close()
}
// This test is not possible until we switch to `git status --porcelain`
// where we can mock the env for the specific git process. This is because
// git2 does not care about our mocking and when we set the real `GIT_DIR`

View File

@ -372,7 +372,12 @@ fn git_status_wsl(context: &Context, conf: &GitStatusConfig) -> Option<String> {
// Ensure this is WSL
// This is lowercase in WSL1 and uppercase in WSL2, just skip the first letter
if !uname().release().contains("icrosoft") {
if !uname()
.ok()?
.release()
.to_string_lossy()
.contains("icrosoft")
{
return None;
}

View File

@ -2,7 +2,6 @@ use super::{Context, Module, ModuleConfig};
use crate::configs::haskell::HaskellConfig;
use crate::formatter::StringFormatter;
use crate::utils;
/// Creates a module with the current Haskell version
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
@ -64,7 +63,7 @@ fn get_snapshot(context: &Context) -> Option<String> {
if !is_stack_project(context) {
return None;
}
let file_contents = utils::read_file(context.current_dir.join("stack.yaml")).ok()?;
let file_contents = context.read_file_from_pwd("stack.yaml")?;
let yaml = yaml_rust::YamlLoader::load_from_str(&file_contents).ok()?;
let version = yaml.first()?["resolver"]
.as_str()

View File

@ -2,7 +2,6 @@ use super::{Context, Module, ModuleConfig};
use crate::configs::nodejs::NodejsConfig;
use crate::formatter::{StringFormatter, VersionFormatter};
use crate::utils;
use once_cell::sync::Lazy;
use regex::Regex;
@ -10,7 +9,6 @@ use semver::Version;
use semver::VersionReq;
use serde_json as json;
use std::ops::Deref;
use std::path::Path;
/// Creates a module with the current Node.js version
pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
@ -45,7 +43,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
})
.map_style(|variable| match variable {
"style" => {
let engines_version = get_engines_version(&context.current_dir);
let engines_version = get_engines_version(context);
let in_engines_range =
check_engines_version(nodejs_version.deref().as_ref()?, engines_version);
if in_engines_range {
@ -87,8 +85,8 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
Some(module)
}
fn get_engines_version(base_dir: &Path) -> Option<String> {
let json_str = utils::read_file(base_dir.join("package.json")).ok()?;
fn get_engines_version(context: &Context) -> Option<String> {
let json_str = context.read_file_from_pwd("package.json")?;
let package_json: json::Value = json::from_str(&json_str).ok()?;
let raw_version = package_json.get("engines")?.get("node")?.as_str()?;
Some(raw_version.to_string())

View File

@ -1,7 +1,6 @@
use super::{Context, Module, ModuleConfig};
use crate::configs::package::PackageConfig;
use crate::formatter::{StringFormatter, VersionFormatter};
use crate::utils;
use ini::Ini;
use quick_xml::events::Event as QXEvent;
@ -44,7 +43,7 @@ pub fn module<'a>(context: &'a Context) -> Option<Module<'a>> {
}
fn get_node_package_version(context: &Context, config: &PackageConfig) -> Option<String> {
let file_contents = utils::read_file(&context.current_dir.join("package.json")).ok()?;
let file_contents = context.read_file_from_pwd("package.json")?;
let package_json: json::Value = json::from_str(&file_contents).ok()?;
if !config.display_private
@ -68,7 +67,7 @@ fn get_node_package_version(context: &Context, config: &PackageConfig) -> Option
}
fn get_poetry_version(context: &Context, config: &PackageConfig) -> Option<String> {
let file_contents = utils::read_file(&context.current_dir.join("pyproject.toml")).ok()?;
let file_contents = context.read_file_from_pwd("pyproject.toml")?;
let poetry_toml: toml::Value = toml::from_str(&file_contents).ok()?;
let raw_version = poetry_toml
.get("tool")?
@ -80,7 +79,7 @@ fn get_poetry_version(context: &Context, config: &PackageConfig) -> Option<Strin
}
fn get_setup_cfg_version(context: &Context, config: &PackageConfig) -> Option<String> {
let file_contents = utils::read_file(context.current_dir.join("setup.cfg")).ok()?;
let file_contents = context.read_file_from_pwd("setup.cfg")?;
let ini = Ini::load_from_str(&file_contents).ok()?;
let raw_version = ini.get_from(Some("metadata"), "version")?;
@ -92,7 +91,7 @@ fn get_setup_cfg_version(context: &Context, config: &PackageConfig) -> Option<St
}
fn get_gradle_version(context: &Context, config: &PackageConfig) -> Option<String> {
let file_contents = utils::read_file(context.current_dir.join("build.gradle")).ok()?;
let file_contents = context.read_file_from_pwd("build.gradle")?;
let re = Regex::new(r#"(?m)^version ['"](?P<version>[^'"]+)['"]$"#).unwrap();
let caps = re.captures(&file_contents)?;
@ -100,7 +99,7 @@ fn get_gradle_version(context: &Context, config: &PackageConfig) -> Option<Strin
}
fn get_composer_version(context: &Context, config: &PackageConfig) -> Option<String> {
let file_contents = utils::read_file(context.current_dir.join("composer.json")).ok()?;
let file_contents = context.read_file_from_pwd("composer.json")?;
let composer_json: json::Value = json::from_str(&file_contents).ok()?;
let raw_version = composer_json.get("version")?.as_str()?;
@ -108,7 +107,7 @@ fn get_composer_version(context: &Context, config: &PackageConfig) -> Option<Str
}
fn get_julia_project_version(context: &Context, config: &PackageConfig) -> Option<String> {
let file_contents = utils::read_file(context.current_dir.join("Project.toml")).ok()?;
let file_contents = context.read_file_from_pwd("Project.toml")?;
let project_toml: toml::Value = toml::from_str(&file_contents).ok()?;
let raw_version = project_toml.get("version")?.as_str()?;
@ -116,7 +115,7 @@ fn get_julia_project_version(context: &Context, config: &PackageConfig) -> Optio
}
fn get_helm_package_version(context: &Context, config: &PackageConfig) -> Option<String> {
let file_contents = utils::read_file(context.current_dir.join("Chart.yaml")).ok()?;
let file_contents = context.read_file_from_pwd("Chart.yaml")?;
let yaml = yaml_rust::YamlLoader::load_from_str(&file_contents).ok()?;
let version = yaml.first()?["version"].as_str()?;
@ -124,7 +123,7 @@ fn get_helm_package_version(context: &Context, config: &PackageConfig) -> Option
}
fn get_mix_version(context: &Context, config: &PackageConfig) -> Option<String> {
let file_contents = utils::read_file(context.current_dir.join("mix.exs")).ok()?;
let file_contents = context.read_file_from_pwd("mix.exs")?;
let re = Regex::new(r#"(?m)version: "(?P<version>[^"]+)""#).unwrap();
let caps = re.captures(&file_contents)?;
@ -132,8 +131,8 @@ fn get_mix_version(context: &Context, config: &PackageConfig) -> Option<String>
}
fn get_maven_version(context: &Context, config: &PackageConfig) -> Option<String> {
let pom_file = utils::read_file(context.current_dir.join("pom.xml")).ok()?;
let mut reader = QXReader::from_str(&pom_file);
let file_contents = context.read_file_from_pwd("pom.xml")?;
let mut reader = QXReader::from_str(&file_contents);
reader.trim_text(true);
let mut buf = vec![];
@ -171,8 +170,8 @@ fn get_maven_version(context: &Context, config: &PackageConfig) -> Option<String
}
fn get_meson_version(context: &Context, config: &PackageConfig) -> Option<String> {
let file_contents = utils::read_file(context.current_dir.join("meson.build"))
.ok()?
let file_contents = context
.read_file_from_pwd("meson.build")?
.split_ascii_whitespace()
.collect::<String>();
@ -183,14 +182,14 @@ fn get_meson_version(context: &Context, config: &PackageConfig) -> Option<String
}
fn get_vmod_version(context: &Context, config: &PackageConfig) -> Option<String> {
let file_contents = utils::read_file(context.current_dir.join("v.mod")).ok()?;
let file_contents = context.read_file_from_pwd("v.mod")?;
let re = Regex::new(r"(?m)^\s*version\s*:\s*'(?P<version>[^']+)'").unwrap();
let caps = re.captures(&file_contents)?;
format_version(&caps["version"], config.version_format)
}
fn get_vpkg_version(context: &Context, config: &PackageConfig) -> Option<String> {
let file_contents = utils::read_file(context.current_dir.join("vpkg.json")).ok()?;
let file_contents = context.read_file_from_pwd("vpkg.json")?;
let vpkg_json: json::Value = json::from_str(&file_contents).ok()?;
let raw_version = vpkg_json.get("version")?.as_str()?;
@ -198,14 +197,14 @@ fn get_vpkg_version(context: &Context, config: &PackageConfig) -> Option<String>
}
fn get_sbt_version(context: &Context, config: &PackageConfig) -> Option<String> {
let file_contents = utils::read_file(context.current_dir.join("build.sbt")).ok()?;
let file_contents = context.read_file_from_pwd("build.sbt")?;
let re = Regex::new(r"(?m)^(.*/)*\s*version\s*:=\s*.(?P<version>[\d\.]+)").unwrap();
let caps = re.captures(&file_contents)?;
format_version(&caps["version"], config.version_format)
}
fn get_cargo_version(context: &Context, config: &PackageConfig) -> Option<String> {
let file_contents = utils::read_file(&context.current_dir.join("Cargo.toml")).ok()?;
let file_contents = context.read_file_from_pwd("Cargo.toml")?;
let cargo_toml: toml::Value = toml::from_str(&file_contents).ok()?;
let raw_version = cargo_toml.get("package")?.get("version")?.as_str()?;
@ -231,7 +230,7 @@ fn get_nimble_version(context: &Context, config: &PackageConfig) -> Option<Strin
}
fn get_shard_version(context: &Context, config: &PackageConfig) -> Option<String> {
let file_contents = utils::read_file(&context.current_dir.join("shard.yml")).ok()?;
let file_contents = context.read_file_from_pwd("shard.yml")?;
let data = yaml_rust::YamlLoader::load_from_str(&file_contents).ok()?;
let raw_version = data.first()?["version"].as_str()?;
@ -240,7 +239,7 @@ fn get_shard_version(context: &Context, config: &PackageConfig) -> Option<String
}
fn get_dart_pub_version(context: &Context, config: &PackageConfig) -> Option<String> {
let file_contents = utils::read_file(&context.current_dir.join("pubspec.yaml")).ok()?;
let file_contents = context.read_file_from_pwd("pubspec.yaml")?;
let data = yaml_rust::YamlLoader::load_from_str(&file_contents).ok()?;
let raw_version = data.first()?["version"].as_str()?;

View File

@ -13,8 +13,9 @@ use std::path::Path;
/// 2a) (not implemented on macOS) one of the supplementary groups of the current user is the
/// directory group owner and whether it has write access
/// 3) 'others' part of the access mask has the write access
pub fn is_write_allowed(folder_path: &Path) -> Result<bool, &'static str> {
let meta = fs::metadata(folder_path).map_err(|_| "Unable to stat() directory")?;
pub fn is_write_allowed(folder_path: &Path) -> Result<bool, String> {
let meta =
fs::metadata(folder_path).map_err(|e| format!("Unable to stat() directory: {e:?}"))?;
let perms = meta.permissions().mode();
let euid = Uid::effective();
@ -54,9 +55,9 @@ mod tests {
#[ignore]
fn read_only_test() {
assert_eq!(is_write_allowed(Path::new("/etc")), Ok(false));
assert_eq!(
is_write_allowed(Path::new("/i_dont_exist")),
Err("Unable to stat() directory")
);
assert!(match is_write_allowed(Path::new("/i_dont_exist")) {
Ok(_) => false,
Err(e) => e.starts_with("Unable to stat() directory"),
});
}
}

View File

@ -1,29 +1,32 @@
extern crate winapi;
use std::{ffi::c_void, mem, os::windows::ffi::OsStrExt, path::Path};
use std::mem;
use std::os::windows::ffi::OsStrExt;
use std::path::Path;
use winapi::shared::minwindef::{BOOL, DWORD};
use winapi::um::handleapi;
use winapi::um::processthreadsapi;
use winapi::um::securitybaseapi;
use winapi::um::winnt::{
SecurityImpersonation, BOOLEAN, DACL_SECURITY_INFORMATION, FILE_ALL_ACCESS,
FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE, GENERIC_MAPPING,
GROUP_SECURITY_INFORMATION, HANDLE, LPCWSTR, OWNER_SECURITY_INFORMATION, PRIVILEGE_SET,
STANDARD_RIGHTS_READ, TOKEN_DUPLICATE, TOKEN_IMPERSONATE, TOKEN_QUERY,
use windows::{
core::PCWSTR,
Win32::{
Foundation::{CloseHandle, ERROR_INSUFFICIENT_BUFFER, HANDLE},
Security::{
AccessCheck, DuplicateToken, GetFileSecurityW, MapGenericMask, SecurityImpersonation,
DACL_SECURITY_INFORMATION, GENERIC_MAPPING, GROUP_SECURITY_INFORMATION,
OWNER_SECURITY_INFORMATION, PRIVILEGE_SET, PSECURITY_DESCRIPTOR, TOKEN_DUPLICATE,
TOKEN_IMPERSONATE, TOKEN_QUERY, TOKEN_READ_CONTROL,
},
Storage::FileSystem::{
FILE_ALL_ACCESS, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ, FILE_GENERIC_WRITE,
},
System::Threading::{GetCurrentProcess, OpenProcessToken},
UI::Shell::PathIsNetworkPathW,
},
};
/// Checks if the current user has write access right to the `folder_path`
///
/// First, the function extracts DACL from the given directory and then calls `AccessCheck` against
/// the current process access token and directory's security descriptor.
/// Does not work for network drives and always returns true
pub fn is_write_allowed(folder_path: &Path) -> std::result::Result<bool, &'static str> {
let folder_name: Vec<u16> = folder_path.as_os_str().encode_wide().chain([0]).collect();
pub fn is_write_allowed(folder_path: &Path) -> std::result::Result<bool, String> {
let wpath_vec: Vec<u16> = folder_path.as_os_str().encode_wide().chain([0]).collect();
let wpath = PCWSTR(wpath_vec.as_ptr());
if is_network_path(&folder_name) {
if unsafe { PathIsNetworkPathW(wpath) }.as_bool() {
log::info!(
"Directory '{:?}' is a network drive, unable to check write permissions. See #1506 for details",
folder_path
@ -31,79 +34,84 @@ pub fn is_write_allowed(folder_path: &Path) -> std::result::Result<bool, &'stati
return Ok(true);
}
let mut length: DWORD = 0;
let mut length = 0;
let rc = unsafe {
securitybaseapi::GetFileSecurityW(
folder_name.as_ptr(),
OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
std::ptr::null_mut(),
GetFileSecurityW(
wpath,
(OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION).0,
PSECURITY_DESCRIPTOR::default(),
0,
&mut length,
)
};
if rc != 0 {
return Err(
"GetFileSecurityW returned non-zero when asked for the security descriptor size",
);
// expect ERROR_INSUFFICIENT_BUFFER
match rc.ok() {
Err(e) if e.win32_error() == Some(ERROR_INSUFFICIENT_BUFFER) => (),
result => return Err(format!("GetFileSecurityW returned unexpected return value when asked for the security descriptor size: {:?}", result)),
}
let mut buf: Vec<u8> = Vec::with_capacity(length as usize);
let mut buf = vec![0u8; length as usize];
let psecurity_descriptor = PSECURITY_DESCRIPTOR(buf.as_mut_ptr() as *mut c_void);
let rc = unsafe {
securitybaseapi::GetFileSecurityW(
folder_name.as_ptr(),
OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
buf.as_mut_ptr().cast::<std::ffi::c_void>(),
GetFileSecurityW(
wpath,
(OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION).0,
psecurity_descriptor,
length,
&mut length,
)
};
if rc != 1 {
return Err("GetFileSecurityW failed to retrieve the security descriptor");
if let Err(e) = rc.ok() {
return Err(format!(
"GetFileSecurityW failed to retrieve the security descriptor: {e:?}"
));
}
let mut token: HANDLE = 0 as HANDLE;
let mut token = HANDLE::default();
let rc = unsafe {
processthreadsapi::OpenProcessToken(
processthreadsapi::GetCurrentProcess(),
TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_DUPLICATE | STANDARD_RIGHTS_READ,
OpenProcessToken(
GetCurrentProcess(),
TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_READ_CONTROL,
&mut token,
)
};
if rc != 1 {
return Err("OpenProcessToken failed to retrieve current process' security token");
if let Err(e) = rc.ok() {
return Err(format!(
"OpenProcessToken failed to retrieve current process' security token: {e:?}"
));
}
let mut impersonated_token: HANDLE = 0 as HANDLE;
let rc = unsafe {
securitybaseapi::DuplicateToken(token, SecurityImpersonation, &mut impersonated_token)
};
if rc != 1 {
unsafe { handleapi::CloseHandle(token) };
return Err("DuplicateToken failed");
let mut impersonated_token = HANDLE::default();
let rc = unsafe { DuplicateToken(token, SecurityImpersonation, &mut impersonated_token) };
if let Err(e) = rc.ok() {
unsafe { CloseHandle(token) };
return Err(format!("DuplicateToken failed: {e:?}"));
}
let mut mapping: GENERIC_MAPPING = GENERIC_MAPPING {
GenericRead: FILE_GENERIC_READ,
GenericWrite: FILE_GENERIC_WRITE,
GenericExecute: FILE_GENERIC_EXECUTE,
GenericAll: FILE_ALL_ACCESS,
let mapping = GENERIC_MAPPING {
GenericRead: FILE_GENERIC_READ.0,
GenericWrite: FILE_GENERIC_WRITE.0,
GenericExecute: FILE_GENERIC_EXECUTE.0,
GenericAll: FILE_ALL_ACCESS.0,
};
let mut priviledges: PRIVILEGE_SET = PRIVILEGE_SET::default();
let mut priv_size = mem::size_of::<PRIVILEGE_SET>() as DWORD;
let mut granted_access: DWORD = 0;
let mut access_rights: DWORD = FILE_GENERIC_WRITE;
let mut result: BOOL = 0;
unsafe { securitybaseapi::MapGenericMask(&mut access_rights, &mut mapping) };
let mut priv_size = mem::size_of::<PRIVILEGE_SET>() as _;
let mut granted_access = 0;
let mut access_rights = FILE_GENERIC_WRITE;
let mut result = 0;
unsafe { MapGenericMask(&mut access_rights.0, &mapping) };
let rc = unsafe {
securitybaseapi::AccessCheck(
buf.as_mut_ptr().cast::<std::ffi::c_void>(),
AccessCheck(
psecurity_descriptor,
impersonated_token,
access_rights,
&mut mapping,
access_rights.0,
&mapping,
&mut priviledges,
&mut priv_size,
&mut granted_access,
@ -111,22 +119,13 @@ pub fn is_write_allowed(folder_path: &Path) -> std::result::Result<bool, &'stati
)
};
unsafe {
handleapi::CloseHandle(impersonated_token);
handleapi::CloseHandle(token);
CloseHandle(impersonated_token);
CloseHandle(token);
}
if rc != 1 {
return Err("AccessCheck failed");
if let Err(e) = rc.ok() {
return Err(format!("AccessCheck failed: {e:?}"));
}
Ok(result != 0)
}
#[link(name = "Shlwapi")]
extern "system" {
fn PathIsNetworkPathW(pszPath: LPCWSTR) -> BOOLEAN;
}
fn is_network_path(folder_path: &[u16]) -> bool {
unsafe { PathIsNetworkPathW(folder_path.as_ptr()) == 1 }
}