mirror of https://github.com/astral-sh/uv
Use the `windows` crate facade consistently (#15737)
The initial motivation for this change was that we were using both the `windows`, the `window_sys` and the `windows_core` crate in various places. These crates have slightly unconventional versioning scheme where there is a large workspace with the same version in general, but only some crates get breaking releases when a new breaking release happens, the others stay on the previous breaking version. The `windows` crate is a shim for all three of them, with a single version. This simplifies handling the versions. Using `windows` over `windows_sys` has the advantage of a higher level error interface, we now get a `Result` for all windows API calls instead of C-style int-returns and get-last-error calls. This makes the uv-keyring crate more resilient. We keep using the `windows_registry` crate, which provides a higher level interface to windows registry access.
This commit is contained in:
parent
12764df8b2
commit
cd49e1d11f
|
|
@ -5097,7 +5097,6 @@ dependencies = [
|
||||||
"which",
|
"which",
|
||||||
"whoami",
|
"whoami",
|
||||||
"windows 0.59.0",
|
"windows 0.59.0",
|
||||||
"windows-result",
|
|
||||||
"wiremock",
|
"wiremock",
|
||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
@ -5711,7 +5710,6 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows 0.59.0",
|
"windows 0.59.0",
|
||||||
"windows-core 0.59.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5858,7 +5856,7 @@ dependencies = [
|
||||||
"security-framework",
|
"security-framework",
|
||||||
"thiserror 2.0.16",
|
"thiserror 2.0.16",
|
||||||
"tokio",
|
"tokio",
|
||||||
"windows-sys 0.59.0",
|
"windows 0.59.0",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -6144,9 +6142,8 @@ dependencies = [
|
||||||
"uv-trampoline-builder",
|
"uv-trampoline-builder",
|
||||||
"uv-warnings",
|
"uv-warnings",
|
||||||
"which",
|
"which",
|
||||||
|
"windows 0.59.0",
|
||||||
"windows-registry",
|
"windows-registry",
|
||||||
"windows-result",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -6357,9 +6354,8 @@ dependencies = [
|
||||||
"tracing",
|
"tracing",
|
||||||
"uv-fs",
|
"uv-fs",
|
||||||
"uv-static",
|
"uv-static",
|
||||||
|
"windows 0.59.0",
|
||||||
"windows-registry",
|
"windows-registry",
|
||||||
"windows-result",
|
|
||||||
"windows-sys 0.59.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -195,11 +195,8 @@ uuid = { version = "1.16.0" }
|
||||||
version-ranges = { git = "https://github.com/astral-sh/pubgrub", rev = "06ec5a5f59ffaeb6cf5079c6cb184467da06c9db" }
|
version-ranges = { git = "https://github.com/astral-sh/pubgrub", rev = "06ec5a5f59ffaeb6cf5079c6cb184467da06c9db" }
|
||||||
walkdir = { version = "2.5.0" }
|
walkdir = { version = "2.5.0" }
|
||||||
which = { version = "8.0.0", features = ["regex"] }
|
which = { version = "8.0.0", features = ["regex"] }
|
||||||
windows = { version = "0.59.0", features = ["Win32_Globalization", "Win32_System_Console", "Win32_System_Kernel", "Win32_System_Diagnostics_Debug", "Win32_Storage_FileSystem"] }
|
windows = { version = "0.59.0", features = ["Win32_Globalization", "Win32_Security", "Win32_System_Console", "Win32_System_Kernel", "Win32_System_Diagnostics_Debug", "Win32_Storage_FileSystem", "Win32_System_Registry", "Win32_System_IO", "Win32_System_Ioctl"] }
|
||||||
windows-core = { version = "0.59.0" }
|
|
||||||
windows-registry = { version = "0.5.0" }
|
windows-registry = { version = "0.5.0" }
|
||||||
windows-result = { version = "0.3.0" }
|
|
||||||
windows-sys = { version = "0.59.0", features = ["Win32_Foundation", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Ioctl", "Win32_System_IO", "Win32_System_Registry"] }
|
|
||||||
wiremock = { version = "0.6.4" }
|
wiremock = { version = "0.6.4" }
|
||||||
xz2 = { version = "0.1.7" }
|
xz2 = { version = "0.1.7" }
|
||||||
zeroize = { version = "1.8.1" }
|
zeroize = { version = "1.8.1" }
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ rustix = { workspace = true }
|
||||||
backon = { workspace = true }
|
backon = { workspace = true }
|
||||||
junction = { workspace = true }
|
junction = { workspace = true }
|
||||||
windows = { workspace = true }
|
windows = { workspace = true }
|
||||||
windows-core = { workspace = true }
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use std::path::Path;
|
||||||
fn get_binary_type(path: &Path) -> windows::core::Result<u32> {
|
fn get_binary_type(path: &Path) -> windows::core::Result<u32> {
|
||||||
use std::os::windows::ffi::OsStrExt;
|
use std::os::windows::ffi::OsStrExt;
|
||||||
use windows::Win32::Storage::FileSystem::GetBinaryTypeW;
|
use windows::Win32::Storage::FileSystem::GetBinaryTypeW;
|
||||||
use windows_core::PCWSTR;
|
use windows::core::PCWSTR;
|
||||||
|
|
||||||
// References:
|
// References:
|
||||||
// https://github.com/denoland/deno/blob/01a6379505712be34ebf2cdc874fa7f54a6e9408/runtime/permissions/which.rs#L131-L154
|
// https://github.com/denoland/deno/blob/01a6379505712be34ebf2cdc874fa7f54a6e9408/runtime/permissions/which.rs#L131-L154
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ apple-native = ["dep:security-framework"]
|
||||||
## Use the secret-service on *nix.
|
## Use the secret-service on *nix.
|
||||||
secret-service = ["dep:secret-service"]
|
secret-service = ["dep:secret-service"]
|
||||||
## Use the built-in credential store on Windows
|
## Use the built-in credential store on Windows
|
||||||
windows-native = ["dep:windows-sys", "dep:byteorder"]
|
windows-native = ["dep:windows", "dep:byteorder"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
|
|
@ -33,7 +33,7 @@ secret-service = { workspace = true, features = ["rt-tokio-crypto-rust"], option
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
byteorder = { workspace = true, optional = true }
|
byteorder = { workspace = true, optional = true }
|
||||||
windows-sys = { workspace = true, features = ["Win32_Foundation", "Win32_Security_Credentials"], optional = true }
|
windows = { workspace = true, features = ["Win32_Foundation", "Win32_Security_Credentials"], optional = true }
|
||||||
zeroize = { workspace = true }
|
zeroize = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
||||||
|
|
@ -44,15 +44,16 @@ use byteorder::{ByteOrder, LittleEndian};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::iter::once;
|
use std::iter::once;
|
||||||
use std::str;
|
use std::str;
|
||||||
use windows_sys::Win32::Foundation::{
|
use windows::Win32::Foundation::{
|
||||||
ERROR_BAD_USERNAME, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NO_SUCH_LOGON_SESSION,
|
ERROR_BAD_USERNAME, ERROR_INVALID_FLAGS, ERROR_INVALID_PARAMETER, ERROR_NO_SUCH_LOGON_SESSION,
|
||||||
ERROR_NOT_FOUND, FILETIME, GetLastError,
|
ERROR_NOT_FOUND, FILETIME, WIN32_ERROR,
|
||||||
};
|
};
|
||||||
use windows_sys::Win32::Security::Credentials::{
|
use windows::Win32::Security::Credentials::{
|
||||||
CRED_FLAGS, CRED_MAX_CREDENTIAL_BLOB_SIZE, CRED_MAX_GENERIC_TARGET_NAME_LENGTH,
|
CRED_FLAGS, CRED_MAX_CREDENTIAL_BLOB_SIZE, CRED_MAX_GENERIC_TARGET_NAME_LENGTH,
|
||||||
CRED_MAX_STRING_LENGTH, CRED_MAX_USERNAME_LENGTH, CRED_PERSIST_ENTERPRISE, CRED_TYPE_GENERIC,
|
CRED_MAX_STRING_LENGTH, CRED_MAX_USERNAME_LENGTH, CRED_PERSIST_ENTERPRISE, CRED_TYPE_GENERIC,
|
||||||
CREDENTIAL_ATTRIBUTEW, CREDENTIALW, CredDeleteW, CredFree, CredReadW, CredWriteW,
|
CREDENTIAL_ATTRIBUTEW, CREDENTIALW, CredDeleteW, CredFree, CredReadW, CredWriteW,
|
||||||
};
|
};
|
||||||
|
use windows::core::PWSTR;
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
/// The representation of a Windows Generic credential.
|
/// The representation of a Windows Generic credential.
|
||||||
|
|
@ -160,15 +161,13 @@ impl CredentialApi for WinCredential {
|
||||||
/// credential in the store.
|
/// credential in the store.
|
||||||
async fn delete_credential(&self) -> Result<()> {
|
async fn delete_credential(&self) -> Result<()> {
|
||||||
self.validate_attributes(None, None)?;
|
self.validate_attributes(None, None)?;
|
||||||
let target_name = to_wstr(&self.target_name);
|
let mut target_name = to_wstr(&self.target_name);
|
||||||
let cred_type = CRED_TYPE_GENERIC;
|
let cred_type = CRED_TYPE_GENERIC;
|
||||||
crate::blocking::spawn_blocking(move || {
|
crate::blocking::spawn_blocking(move || {
|
||||||
// SAFETY: Calling Windows API
|
// SAFETY: Calling Windows API
|
||||||
if unsafe { CredDeleteW(target_name.as_ptr(), cred_type, 0) } != 0 {
|
unsafe {
|
||||||
Ok(())
|
CredDeleteW(PWSTR(target_name.as_mut_ptr()), cred_type, None)
|
||||||
} else {
|
.map_err(|err| Error(err).into())
|
||||||
// SAFETY: Calling Windows API
|
|
||||||
Err(Error::from_win32_code(unsafe { GetLastError() }).into())
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
|
|
@ -264,22 +263,20 @@ impl WinCredential {
|
||||||
let credential = CREDENTIALW {
|
let credential = CREDENTIALW {
|
||||||
Flags: flags,
|
Flags: flags,
|
||||||
Type: cred_type,
|
Type: cred_type,
|
||||||
TargetName: target_name.as_mut_ptr(),
|
TargetName: PWSTR(target_name.as_mut_ptr()),
|
||||||
Comment: comment.as_mut_ptr(),
|
Comment: PWSTR(comment.as_mut_ptr()),
|
||||||
LastWritten: last_written,
|
LastWritten: last_written,
|
||||||
CredentialBlobSize: blob_len,
|
CredentialBlobSize: blob_len,
|
||||||
CredentialBlob: blob.as_mut_ptr(),
|
CredentialBlob: blob.as_mut_ptr(),
|
||||||
Persist: persist,
|
Persist: persist,
|
||||||
AttributeCount: attribute_count,
|
AttributeCount: attribute_count,
|
||||||
Attributes: attributes,
|
Attributes: attributes,
|
||||||
TargetAlias: target_alias.as_mut_ptr(),
|
TargetAlias: PWSTR(target_alias.as_mut_ptr()),
|
||||||
UserName: username.as_mut_ptr(),
|
UserName: PWSTR(username.as_mut_ptr()),
|
||||||
};
|
};
|
||||||
// SAFETY: Calling Windows API
|
// SAFETY: Calling Windows API
|
||||||
let result = match unsafe { CredWriteW(&raw const credential, 0) } {
|
let result =
|
||||||
0 => Err(Error::from_win32_code(unsafe { GetLastError() }).into()),
|
unsafe { CredWriteW(&raw const credential, 0) }.map_err(|err| Error(err).into());
|
||||||
_ => Ok(()),
|
|
||||||
};
|
|
||||||
// erase the copy of the secret
|
// erase the copy of the secret
|
||||||
blob.zeroize();
|
blob.zeroize();
|
||||||
result
|
result
|
||||||
|
|
@ -300,31 +297,33 @@ impl WinCredential {
|
||||||
T: Send + 'static,
|
T: Send + 'static,
|
||||||
{
|
{
|
||||||
self.validate_attributes(None, None)?;
|
self.validate_attributes(None, None)?;
|
||||||
let target_name = to_wstr(&self.target_name);
|
let mut target_name = to_wstr(&self.target_name);
|
||||||
crate::blocking::spawn_blocking(move || {
|
crate::blocking::spawn_blocking(move || {
|
||||||
let mut p_credential = std::ptr::null_mut();
|
let mut p_credential = std::ptr::null_mut();
|
||||||
// at this point, p_credential is just a pointer to nowhere.
|
// at this point, p_credential is just a pointer to nowhere.
|
||||||
// The allocation happens in the `CredReadW` call below.
|
// The allocation happens in the `CredReadW` call below.
|
||||||
let cred_type = CRED_TYPE_GENERIC;
|
let cred_type = CRED_TYPE_GENERIC;
|
||||||
// SAFETY: Calling windows API
|
// SAFETY: Calling windows API
|
||||||
let result =
|
unsafe {
|
||||||
unsafe { CredReadW(target_name.as_ptr(), cred_type, 0, &raw mut p_credential) };
|
CredReadW(
|
||||||
if result == 0 {
|
PWSTR(target_name.as_mut_ptr()),
|
||||||
// `CredReadW` failed, so no allocation has been done, so no free needs to be done
|
cred_type,
|
||||||
Err(Error::from_win32_code(unsafe { GetLastError() }).into())
|
None,
|
||||||
} else {
|
&raw mut p_credential,
|
||||||
// SAFETY: `CredReadW` succeeded, so p_credential points at an allocated credential. Apply
|
)
|
||||||
// the passed extractor function to it.
|
|
||||||
let ref_cred: &mut CREDENTIALW = unsafe { &mut *p_credential };
|
|
||||||
let result = f(ref_cred);
|
|
||||||
// Finally, we erase the secret and free the allocated credential.
|
|
||||||
erase_secret(ref_cred);
|
|
||||||
let p_credential = p_credential;
|
|
||||||
// SAFETY: `CredReadW` succeeded, so p_credential points at an allocated credential.
|
|
||||||
// Free the allocation.
|
|
||||||
unsafe { CredFree(p_credential.cast()) }
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
|
.map_err(Error)?;
|
||||||
|
// SAFETY: `CredReadW` succeeded, so p_credential points at an allocated credential. Apply
|
||||||
|
// the passed extractor function to it.
|
||||||
|
let ref_cred: &mut CREDENTIALW = unsafe { &mut *p_credential };
|
||||||
|
let result = f(ref_cred);
|
||||||
|
// Finally, we erase the secret and free the allocated credential.
|
||||||
|
erase_secret(ref_cred);
|
||||||
|
let p_credential = p_credential;
|
||||||
|
// SAFETY: `CredReadW` succeeded, so p_credential points at an allocated credential.
|
||||||
|
// Free the allocation.
|
||||||
|
unsafe { CredFree(p_credential.cast()) }
|
||||||
|
result
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
@ -332,10 +331,10 @@ impl WinCredential {
|
||||||
#[allow(clippy::unnecessary_wraps)]
|
#[allow(clippy::unnecessary_wraps)]
|
||||||
fn extract_credential(w_credential: &CREDENTIALW) -> Result<Self> {
|
fn extract_credential(w_credential: &CREDENTIALW) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
username: unsafe { from_wstr(w_credential.UserName) },
|
username: unsafe { from_wstr(w_credential.UserName.as_ptr()) },
|
||||||
target_name: unsafe { from_wstr(w_credential.TargetName) },
|
target_name: unsafe { from_wstr(w_credential.TargetName.as_ptr()) },
|
||||||
target_alias: unsafe { from_wstr(w_credential.TargetAlias) },
|
target_alias: unsafe { from_wstr(w_credential.TargetAlias.as_ptr()) },
|
||||||
comment: unsafe { from_wstr(w_credential.Comment) },
|
comment: unsafe { from_wstr(w_credential.Comment.as_ptr()) },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -475,34 +474,40 @@ unsafe fn from_wstr(ws: *const u16) -> String {
|
||||||
|
|
||||||
/// Windows error codes are `DWORDS` which are 32-bit unsigned ints.
|
/// Windows error codes are `DWORDS` which are 32-bit unsigned ints.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Error(pub u32);
|
pub struct Error(windows::core::Error);
|
||||||
|
|
||||||
impl Error {
|
impl From<WIN32_ERROR> for Error {
|
||||||
/// Create a Windows error from a Win32 error code.
|
fn from(error: WIN32_ERROR) -> Self {
|
||||||
pub fn from_win32_code(code: u32) -> Self {
|
Self(windows::core::Error::from(error))
|
||||||
Self(code)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Error> for ErrorCode {
|
impl From<Error> for ErrorCode {
|
||||||
fn from(err: Error) -> Self {
|
fn from(err: Error) -> Self {
|
||||||
match err.0 {
|
if err.0 == ERROR_NOT_FOUND.into() {
|
||||||
ERROR_NOT_FOUND => Self::NoEntry,
|
Self::NoEntry
|
||||||
ERROR_NO_SUCH_LOGON_SESSION => Self::NoStorageAccess(Box::new(err)),
|
} else if err.0 == ERROR_NO_SUCH_LOGON_SESSION.into() {
|
||||||
_ => Self::PlatformFailure(Box::new(err)),
|
Self::NoStorageAccess(Box::new(err))
|
||||||
|
} else {
|
||||||
|
Self::PlatformFailure(Box::new(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Error {
|
impl std::fmt::Display for Error {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||||
match self.0 {
|
if self.0 == ERROR_NO_SUCH_LOGON_SESSION.into() {
|
||||||
ERROR_NO_SUCH_LOGON_SESSION => write!(f, "Windows ERROR_NO_SUCH_LOGON_SESSION"),
|
write!(f, "Windows ERROR_NO_SUCH_LOGON_SESSION")
|
||||||
ERROR_NOT_FOUND => write!(f, "Windows ERROR_NOT_FOUND"),
|
} else if self.0 == ERROR_NOT_FOUND.into() {
|
||||||
ERROR_BAD_USERNAME => write!(f, "Windows ERROR_BAD_USERNAME"),
|
write!(f, "Windows ERROR_NOT_FOUND")
|
||||||
ERROR_INVALID_FLAGS => write!(f, "Windows ERROR_INVALID_FLAGS"),
|
} else if self.0 == ERROR_BAD_USERNAME.into() {
|
||||||
ERROR_INVALID_PARAMETER => write!(f, "Windows ERROR_INVALID_PARAMETER"),
|
write!(f, "Windows ERROR_BAD_USERNAME")
|
||||||
err => write!(f, "Windows error code {err}"),
|
} else if self.0 == ERROR_INVALID_FLAGS.into() {
|
||||||
|
write!(f, "Windows ERROR_INVALID_FLAGS")
|
||||||
|
} else if self.0 == ERROR_INVALID_PARAMETER.into() {
|
||||||
|
write!(f, "Windows ERROR_INVALID_PARAMETER")
|
||||||
|
} else {
|
||||||
|
write!(f, "Windows error code {}", self.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -545,18 +550,18 @@ mod tests {
|
||||||
let attribute_count = 0;
|
let attribute_count = 0;
|
||||||
let attributes: *mut CREDENTIAL_ATTRIBUTEW = std::ptr::null_mut();
|
let attributes: *mut CREDENTIAL_ATTRIBUTEW = std::ptr::null_mut();
|
||||||
CREDENTIALW {
|
CREDENTIALW {
|
||||||
Flags: 0,
|
Flags: CRED_FLAGS(0),
|
||||||
Type: CRED_TYPE_GENERIC,
|
Type: CRED_TYPE_GENERIC,
|
||||||
TargetName: std::ptr::null_mut(),
|
TargetName: PWSTR::null(),
|
||||||
Comment: std::ptr::null_mut(),
|
Comment: PWSTR::null(),
|
||||||
LastWritten: last_written,
|
LastWritten: last_written,
|
||||||
CredentialBlobSize: password.len() as u32,
|
CredentialBlobSize: password.len() as u32,
|
||||||
CredentialBlob: password.as_mut_ptr(),
|
CredentialBlob: password.as_mut_ptr(),
|
||||||
Persist: CRED_PERSIST_ENTERPRISE,
|
Persist: CRED_PERSIST_ENTERPRISE,
|
||||||
AttributeCount: attribute_count,
|
AttributeCount: attribute_count,
|
||||||
Attributes: attributes,
|
Attributes: attributes,
|
||||||
TargetAlias: std::ptr::null_mut(),
|
TargetAlias: PWSTR::null(),
|
||||||
UserName: std::ptr::null_mut(),
|
UserName: PWSTR::null(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// the first malformed sequence can't be UTF-16 because it has an odd number of bytes.
|
// the first malformed sequence can't be UTF-16 because it has an odd number of bytes.
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,7 @@ once_cell = { workspace = true }
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
windows-registry = { workspace = true }
|
windows-registry = { workspace = true }
|
||||||
windows-result = { workspace = true }
|
windows = { workspace = true }
|
||||||
windows-sys = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,6 @@ use std::{env, io, iter};
|
||||||
use std::{path::Path, path::PathBuf, str::FromStr};
|
use std::{path::Path, path::PathBuf, str::FromStr};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::{debug, instrument, trace};
|
use tracing::{debug, instrument, trace};
|
||||||
use uv_preview::Preview;
|
|
||||||
use which::{which, which_all};
|
|
||||||
|
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_fs::which::is_executable;
|
use uv_fs::which::is_executable;
|
||||||
|
|
@ -18,8 +15,10 @@ use uv_pep440::{
|
||||||
LowerBound, Prerelease, UpperBound, Version, VersionSpecifier, VersionSpecifiers,
|
LowerBound, Prerelease, UpperBound, Version, VersionSpecifier, VersionSpecifiers,
|
||||||
release_specifiers_to_ranges,
|
release_specifiers_to_ranges,
|
||||||
};
|
};
|
||||||
|
use uv_preview::Preview;
|
||||||
use uv_static::EnvVars;
|
use uv_static::EnvVars;
|
||||||
use uv_warnings::warn_user_once;
|
use uv_warnings::warn_user_once;
|
||||||
|
use which::{which, which_all};
|
||||||
|
|
||||||
use crate::downloads::{PlatformRequest, PythonDownloadRequest};
|
use crate::downloads::{PlatformRequest, PythonDownloadRequest};
|
||||||
use crate::implementation::ImplementationName;
|
use crate::implementation::ImplementationName;
|
||||||
|
|
@ -251,7 +250,7 @@ pub enum Error {
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
#[error("Failed to query installed Python versions from the Windows registry")]
|
#[error("Failed to query installed Python versions from the Windows registry")]
|
||||||
RegistryError(#[from] windows_result::Error),
|
RegistryError(#[from] windows::core::Error),
|
||||||
|
|
||||||
/// An invalid version request was given
|
/// An invalid version request was given
|
||||||
#[error("Invalid version request: {0}")]
|
#[error("Invalid version request: {0}")]
|
||||||
|
|
@ -1502,13 +1501,15 @@ fn warn_on_unsupported_python(interpreter: &Interpreter) {
|
||||||
pub(crate) fn is_windows_store_shim(path: &Path) -> bool {
|
pub(crate) fn is_windows_store_shim(path: &Path) -> bool {
|
||||||
use std::os::windows::fs::MetadataExt;
|
use std::os::windows::fs::MetadataExt;
|
||||||
use std::os::windows::prelude::OsStrExt;
|
use std::os::windows::prelude::OsStrExt;
|
||||||
use windows_sys::Win32::Foundation::{CloseHandle, INVALID_HANDLE_VALUE};
|
use windows::Win32::Foundation::CloseHandle;
|
||||||
use windows_sys::Win32::Storage::FileSystem::{
|
use windows::Win32::Storage::FileSystem::{
|
||||||
CreateFileW, FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS,
|
CreateFileW, FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS,
|
||||||
FILE_FLAG_OPEN_REPARSE_POINT, MAXIMUM_REPARSE_DATA_BUFFER_SIZE, OPEN_EXISTING,
|
FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_MODE, MAXIMUM_REPARSE_DATA_BUFFER_SIZE,
|
||||||
|
OPEN_EXISTING,
|
||||||
};
|
};
|
||||||
use windows_sys::Win32::System::IO::DeviceIoControl;
|
use windows::Win32::System::IO::DeviceIoControl;
|
||||||
use windows_sys::Win32::System::Ioctl::FSCTL_GET_REPARSE_POINT;
|
use windows::Win32::System::Ioctl::FSCTL_GET_REPARSE_POINT;
|
||||||
|
use windows::core::PCWSTR;
|
||||||
|
|
||||||
// The path must be absolute.
|
// The path must be absolute.
|
||||||
if !path.is_absolute() {
|
if !path.is_absolute() {
|
||||||
|
|
@ -1553,7 +1554,7 @@ pub(crate) fn is_windows_store_shim(path: &Path) -> bool {
|
||||||
let Ok(md) = fs_err::symlink_metadata(path) else {
|
let Ok(md) = fs_err::symlink_metadata(path) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
if md.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT == 0 {
|
if md.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0 == 0 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1567,19 +1568,19 @@ pub(crate) fn is_windows_store_shim(path: &Path) -> bool {
|
||||||
#[allow(unsafe_code)]
|
#[allow(unsafe_code)]
|
||||||
let reparse_handle = unsafe {
|
let reparse_handle = unsafe {
|
||||||
CreateFileW(
|
CreateFileW(
|
||||||
path_encoded.as_mut_ptr(),
|
PCWSTR(path_encoded.as_mut_ptr()),
|
||||||
0,
|
0,
|
||||||
0,
|
FILE_SHARE_MODE(0),
|
||||||
std::ptr::null_mut(),
|
None,
|
||||||
OPEN_EXISTING,
|
OPEN_EXISTING,
|
||||||
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
|
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
|
||||||
std::ptr::null_mut(),
|
None,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
if reparse_handle == INVALID_HANDLE_VALUE {
|
let Ok(reparse_handle) = reparse_handle else {
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
let mut buf = [0u16; MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize];
|
let mut buf = [0u16; MAXIMUM_REPARSE_DATA_BUFFER_SIZE as usize];
|
||||||
let mut bytes_returned = 0;
|
let mut bytes_returned = 0;
|
||||||
|
|
@ -1590,19 +1591,20 @@ pub(crate) fn is_windows_store_shim(path: &Path) -> bool {
|
||||||
DeviceIoControl(
|
DeviceIoControl(
|
||||||
reparse_handle,
|
reparse_handle,
|
||||||
FSCTL_GET_REPARSE_POINT,
|
FSCTL_GET_REPARSE_POINT,
|
||||||
std::ptr::null_mut(),
|
None,
|
||||||
0,
|
0,
|
||||||
buf.as_mut_ptr().cast(),
|
Some(buf.as_mut_ptr().cast()),
|
||||||
buf.len() as u32 * 2,
|
buf.len() as u32 * 2,
|
||||||
&raw mut bytes_returned,
|
Some(&raw mut bytes_returned),
|
||||||
std::ptr::null_mut(),
|
None,
|
||||||
) != 0
|
)
|
||||||
|
.is_ok()
|
||||||
};
|
};
|
||||||
|
|
||||||
// SAFETY: The handle is valid.
|
// SAFETY: The handle is valid.
|
||||||
#[allow(unsafe_code)]
|
#[allow(unsafe_code)]
|
||||||
unsafe {
|
unsafe {
|
||||||
CloseHandle(reparse_handle);
|
let _ = CloseHandle(reparse_handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the operation failed, assume it's not a reparse point.
|
// If the operation failed, assume it's not a reparse point.
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use windows_sys::Win32::Foundation::{APPMODEL_ERROR_NO_PACKAGE, ERROR_CANT_ACCESS_FILE};
|
use windows::Win32::Foundation::{APPMODEL_ERROR_NO_PACKAGE, ERROR_CANT_ACCESS_FILE, WIN32_ERROR};
|
||||||
|
|
||||||
/// A Python executable and its associated platform markers.
|
/// A Python executable and its associated platform markers.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -928,8 +928,10 @@ impl InterpreterInfo {
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
if let Some(APPMODEL_ERROR_NO_PACKAGE | ERROR_CANT_ACCESS_FILE) =
|
if let Some(APPMODEL_ERROR_NO_PACKAGE | ERROR_CANT_ACCESS_FILE) = err
|
||||||
err.raw_os_error().and_then(|code| u32::try_from(code).ok())
|
.raw_os_error()
|
||||||
|
.and_then(|code| u32::try_from(code).ok())
|
||||||
|
.map(WIN32_ERROR)
|
||||||
{
|
{
|
||||||
// These error codes are returned if the Python interpreter is a corrupt MSIX
|
// These error codes are returned if the Python interpreter is a corrupt MSIX
|
||||||
// package, which we want to differentiate from a typical spawn failure.
|
// package, which we want to differentiate from a typical spawn failure.
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ use thiserror::Error;
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
use uv_preview::{Preview, PreviewFeatures};
|
use uv_preview::{Preview, PreviewFeatures};
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
|
use windows::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
|
||||||
|
|
||||||
use uv_fs::{LockedFile, Simplified, replace_symlink, symlink_or_copy_file};
|
use uv_fs::{LockedFile, Simplified, replace_symlink, symlink_or_copy_file};
|
||||||
use uv_platform::{Error as PlatformError, Os};
|
use uv_platform::{Error as PlatformError, Os};
|
||||||
|
|
@ -857,7 +857,7 @@ impl PythonMinorVersionLink {
|
||||||
.is_ok_and(|metadata| {
|
.is_ok_and(|metadata| {
|
||||||
// Check that this is a reparse point, which indicates this
|
// Check that this is a reparse point, which indicates this
|
||||||
// is a symlink or junction.
|
// is a symlink or junction.
|
||||||
(metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT) != 0
|
(metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT.0) != 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,10 @@ use thiserror::Error;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
use uv_platform::Arch;
|
use uv_platform::Arch;
|
||||||
use uv_warnings::{warn_user, warn_user_once};
|
use uv_warnings::{warn_user, warn_user_once};
|
||||||
|
use windows::Win32::Foundation::ERROR_FILE_NOT_FOUND;
|
||||||
|
use windows::Win32::System::Registry::{KEY_WOW64_32KEY, KEY_WOW64_64KEY};
|
||||||
|
use windows::core::HRESULT;
|
||||||
use windows_registry::{CURRENT_USER, HSTRING, Key, LOCAL_MACHINE, Value};
|
use windows_registry::{CURRENT_USER, HSTRING, Key, LOCAL_MACHINE, Value};
|
||||||
use windows_result::HRESULT;
|
|
||||||
use windows_sys::Win32::Foundation::ERROR_FILE_NOT_FOUND;
|
|
||||||
use windows_sys::Win32::System::Registry::{KEY_WOW64_32KEY, KEY_WOW64_64KEY};
|
|
||||||
|
|
||||||
/// Code returned when the registry key doesn't exist.
|
|
||||||
const ERROR_NOT_FOUND: HRESULT = HRESULT::from_win32(ERROR_FILE_NOT_FOUND);
|
|
||||||
|
|
||||||
/// A Python interpreter found in the Windows registry through PEP 514 or from a known Microsoft
|
/// A Python interpreter found in the Windows registry through PEP 514 or from a known Microsoft
|
||||||
/// Store path.
|
/// Store path.
|
||||||
|
|
@ -32,7 +29,7 @@ pub(crate) struct WindowsPython {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find all Pythons registered in the Windows registry following PEP 514.
|
/// Find all Pythons registered in the Windows registry following PEP 514.
|
||||||
pub(crate) fn registry_pythons() -> Result<Vec<WindowsPython>, windows_result::Error> {
|
pub(crate) fn registry_pythons() -> Result<Vec<WindowsPython>, windows::core::Error> {
|
||||||
let mut registry_pythons = Vec::new();
|
let mut registry_pythons = Vec::new();
|
||||||
// Prefer `HKEY_CURRENT_USER` over `HKEY_LOCAL_MACHINE`.
|
// Prefer `HKEY_CURRENT_USER` over `HKEY_LOCAL_MACHINE`.
|
||||||
// By default, a 64-bit program does not see a 32-bit global (HKLM) installation of Python in
|
// By default, a 64-bit program does not see a 32-bit global (HKLM) installation of Python in
|
||||||
|
|
@ -47,7 +44,7 @@ pub(crate) fn registry_pythons() -> Result<Vec<WindowsPython>, windows_result::E
|
||||||
let mut open_options = root_key.options();
|
let mut open_options = root_key.options();
|
||||||
open_options.read();
|
open_options.read();
|
||||||
if let Some(access_modifier) = access_modifier {
|
if let Some(access_modifier) = access_modifier {
|
||||||
open_options.access(access_modifier);
|
open_options.access(access_modifier.0);
|
||||||
}
|
}
|
||||||
let Ok(key_python) = open_options.open(r"Software\Python") else {
|
let Ok(key_python) = open_options.open(r"Software\Python") else {
|
||||||
continue;
|
continue;
|
||||||
|
|
@ -131,7 +128,7 @@ pub enum ManagedPep514Error {
|
||||||
#[error("Windows has an unknown pointer width for arch: `{_0}`")]
|
#[error("Windows has an unknown pointer width for arch: `{_0}`")]
|
||||||
InvalidPointerSize(Arch),
|
InvalidPointerSize(Arch),
|
||||||
#[error("Failed to write registry entry: {0}")]
|
#[error("Failed to write registry entry: {0}")]
|
||||||
WriteError(#[from] windows_result::Error),
|
WriteError(#[from] windows::core::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register a managed Python installation in the Windows registry following PEP 514.
|
/// Register a managed Python installation in the Windows registry following PEP 514.
|
||||||
|
|
@ -216,7 +213,7 @@ pub fn remove_registry_entry<'a>(
|
||||||
if all {
|
if all {
|
||||||
debug!("Removing registry key HKCU:\\{}", astral_key);
|
debug!("Removing registry key HKCU:\\{}", astral_key);
|
||||||
if let Err(err) = CURRENT_USER.remove_tree(&astral_key) {
|
if let Err(err) = CURRENT_USER.remove_tree(&astral_key) {
|
||||||
if err.code() == ERROR_NOT_FOUND {
|
if err.code() == HRESULT::from(ERROR_FILE_NOT_FOUND) {
|
||||||
debug!("No registry entries to remove, no registry key {astral_key}");
|
debug!("No registry entries to remove, no registry key {astral_key}");
|
||||||
} else {
|
} else {
|
||||||
warn_user!("Failed to clear registry entries under {astral_key}: {err}");
|
warn_user!("Failed to clear registry entries under {astral_key}: {err}");
|
||||||
|
|
@ -230,7 +227,7 @@ pub fn remove_registry_entry<'a>(
|
||||||
let python_entry = format!("{astral_key}\\{python_tag}");
|
let python_entry = format!("{astral_key}\\{python_tag}");
|
||||||
debug!("Removing registry key HKCU:\\{}", python_entry);
|
debug!("Removing registry key HKCU:\\{}", python_entry);
|
||||||
if let Err(err) = CURRENT_USER.remove_tree(&python_entry) {
|
if let Err(err) = CURRENT_USER.remove_tree(&python_entry) {
|
||||||
if err.code() == ERROR_NOT_FOUND {
|
if err.code() == HRESULT::from(ERROR_FILE_NOT_FOUND) {
|
||||||
debug!(
|
debug!(
|
||||||
"No registry entries to remove for {}, no registry key {}",
|
"No registry entries to remove for {}, no registry key {}",
|
||||||
installation.key(),
|
installation.key(),
|
||||||
|
|
@ -256,7 +253,7 @@ pub fn remove_orphan_registry_entries(installations: &[ManagedPythonInstallation
|
||||||
let astral_key = format!("Software\\Python\\{COMPANY_KEY}");
|
let astral_key = format!("Software\\Python\\{COMPANY_KEY}");
|
||||||
let key = match CURRENT_USER.open(&astral_key) {
|
let key = match CURRENT_USER.open(&astral_key) {
|
||||||
Ok(subkeys) => subkeys,
|
Ok(subkeys) => subkeys,
|
||||||
Err(err) if err.code() == ERROR_NOT_FOUND => {
|
Err(err) if err.code() == HRESULT::from(ERROR_FILE_NOT_FOUND) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|
@ -268,7 +265,7 @@ pub fn remove_orphan_registry_entries(installations: &[ManagedPythonInstallation
|
||||||
// Separate assignment since `keys()` creates a borrow.
|
// Separate assignment since `keys()` creates a borrow.
|
||||||
let subkeys = match key.keys() {
|
let subkeys = match key.keys() {
|
||||||
Ok(subkeys) => subkeys,
|
Ok(subkeys) => subkeys,
|
||||||
Err(err) if err.code() == ERROR_NOT_FOUND => {
|
Err(err) if err.code() == HRESULT::from(ERROR_FILE_NOT_FOUND) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|
@ -284,7 +281,7 @@ pub fn remove_orphan_registry_entries(installations: &[ManagedPythonInstallation
|
||||||
let python_entry = format!("{astral_key}\\{subkey}");
|
let python_entry = format!("{astral_key}\\{subkey}");
|
||||||
debug!("Removing orphan registry key HKCU:\\{}", python_entry);
|
debug!("Removing orphan registry key HKCU:\\{}", python_entry);
|
||||||
if let Err(err) = CURRENT_USER.remove_tree(&python_entry) {
|
if let Err(err) = CURRENT_USER.remove_tree(&python_entry) {
|
||||||
if err.code() == ERROR_NOT_FOUND {
|
if err.code() == HRESULT::from(ERROR_FILE_NOT_FOUND) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// TODO(konsti): We don't have an installation key here.
|
// TODO(konsti): We don't have an installation key here.
|
||||||
|
|
|
||||||
|
|
@ -24,9 +24,8 @@ tracing = { workspace = true }
|
||||||
nix = { workspace = true }
|
nix = { workspace = true }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
windows = { workspace = true }
|
||||||
windows-registry = { workspace = true }
|
windows-registry = { workspace = true }
|
||||||
windows-result = { workspace = true }
|
|
||||||
windows-sys = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
fs-err = { workspace = true }
|
fs-err = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ use std::path::Path;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
|
use windows::Win32::Foundation::{ERROR_FILE_NOT_FOUND, ERROR_INVALID_DATA};
|
||||||
|
use windows::core::HRESULT;
|
||||||
use windows_registry::{CURRENT_USER, HSTRING};
|
use windows_registry::{CURRENT_USER, HSTRING};
|
||||||
use windows_result::HRESULT;
|
|
||||||
use windows_sys::Win32::Foundation::{ERROR_FILE_NOT_FOUND, ERROR_INVALID_DATA};
|
|
||||||
|
|
||||||
use uv_static::EnvVars;
|
use uv_static::EnvVars;
|
||||||
|
|
||||||
|
|
@ -60,13 +60,11 @@ fn get_windows_path_var() -> anyhow::Result<Option<HSTRING>> {
|
||||||
let reg_value = environment.get_hstring(EnvVars::PATH);
|
let reg_value = environment.get_hstring(EnvVars::PATH);
|
||||||
match reg_value {
|
match reg_value {
|
||||||
Ok(reg_value) => Ok(Some(reg_value)),
|
Ok(reg_value) => Ok(Some(reg_value)),
|
||||||
Err(err) if err.code() == HRESULT::from_win32(ERROR_INVALID_DATA) => {
|
Err(err) if err.code() == HRESULT::from(ERROR_INVALID_DATA) => {
|
||||||
warn!("`HKEY_CURRENT_USER\\Environment\\PATH` is a non-string");
|
warn!("`HKEY_CURRENT_USER\\Environment\\PATH` is a non-string");
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
Err(err) if err.code() == HRESULT::from_win32(ERROR_FILE_NOT_FOUND) => {
|
Err(err) if err.code() == HRESULT::from(ERROR_FILE_NOT_FOUND) => Ok(Some(HSTRING::new())),
|
||||||
Ok(Some(HSTRING::new()))
|
|
||||||
}
|
|
||||||
Err(err) => Err(err.into()),
|
Err(err) => Err(err.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,6 @@ zip = { workspace = true }
|
||||||
arrayvec = { workspace = true }
|
arrayvec = { workspace = true }
|
||||||
self-replace = { workspace = true }
|
self-replace = { workspace = true }
|
||||||
windows = { workspace = true }
|
windows = { workspace = true }
|
||||||
windows-result = { workspace = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_cmd = { workspace = true }
|
assert_cmd = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -54,11 +54,11 @@ enum ExceptionSafeStderr {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExceptionSafeStderr {
|
impl ExceptionSafeStderr {
|
||||||
fn new() -> Result<Self, windows_result::Error> {
|
fn new() -> Result<Self, windows::core::Error> {
|
||||||
// SAFETY: winapi call, no interesting parameters
|
// SAFETY: winapi call, no interesting parameters
|
||||||
let handle = unsafe { GetStdHandle(STD_ERROR_HANDLE) }?;
|
let handle = unsafe { GetStdHandle(STD_ERROR_HANDLE) }?;
|
||||||
if handle.is_invalid() {
|
if handle.is_invalid() {
|
||||||
return Err(windows_result::Error::empty());
|
return Err(windows::core::Error::empty());
|
||||||
}
|
}
|
||||||
let mut mode = CONSOLE_MODE::default();
|
let mut mode = CONSOLE_MODE::default();
|
||||||
// SAFETY: winapi calls, no interesting parameters
|
// SAFETY: winapi calls, no interesting parameters
|
||||||
|
|
@ -73,7 +73,7 @@ impl ExceptionSafeStderr {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_winerror(&mut self, s: &str) -> Result<(), windows_result::Error> {
|
fn write_winerror(&mut self, s: &str) -> Result<(), windows::core::Error> {
|
||||||
match self {
|
match self {
|
||||||
Self::WriteConsole(handle) => {
|
Self::WriteConsole(handle) => {
|
||||||
// According to comments in the ReactOS source, NT's behavior is that writes of 80
|
// According to comments in the ReactOS source, NT's behavior is that writes of 80
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue