Skip to content

Commit

Permalink
Replace winapi with windows-sys
Browse files Browse the repository at this point in the history
  • Loading branch information
vthib committed Apr 5, 2023
1 parent 95d03a1 commit c7955a7
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 80 deletions.
9 changes: 5 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ keywords = ["dlopen", "load", "shared", "dylib"]
categories = ["api-bindings"]
rust-version = "1.40.0"

[target.'cfg(windows)'.dependencies.winapi]
version = "0.3"
[target.'cfg(windows)'.dependencies.windows-sys]
version = "0.48"
features = [
"errhandlingapi",
"libloaderapi",
"Win32_Foundation",
"Win32_System_Diagnostics_Debug",
"Win32_System_LibraryLoader",
]

[target.'cfg(unix)'.dependencies.cfg-if]
Expand Down
135 changes: 70 additions & 65 deletions src/os/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,38 +2,44 @@
// same rustdoc build visible.
#[cfg(all(libloading_docs, not(windows)))]
mod windows_imports {
pub(super) enum WORD {}
pub(super) struct DWORD;
pub(super) enum HMODULE {}
pub(super) enum FARPROC {}
#[allow(non_camel_case_types)]
pub(super) enum THREAD_ERROR_MODE {}
#[allow(non_camel_case_types)]
pub(super) struct LOAD_LIBRARY_FLAGS;

pub(super) mod consts {
use super::DWORD;
pub(crate) const LOAD_IGNORE_CODE_AUTHZ_LEVEL: DWORD = DWORD;
pub(crate) const LOAD_LIBRARY_AS_DATAFILE: DWORD = DWORD;
pub(crate) const LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE: DWORD = DWORD;
pub(crate) const LOAD_LIBRARY_AS_IMAGE_RESOURCE: DWORD = DWORD;
pub(crate) const LOAD_LIBRARY_SEARCH_APPLICATION_DIR: DWORD = DWORD;
pub(crate) const LOAD_LIBRARY_SEARCH_DEFAULT_DIRS: DWORD = DWORD;
pub(crate) const LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR: DWORD = DWORD;
pub(crate) const LOAD_LIBRARY_SEARCH_SYSTEM32: DWORD = DWORD;
pub(crate) const LOAD_LIBRARY_SEARCH_USER_DIRS: DWORD = DWORD;
pub(crate) const LOAD_WITH_ALTERED_SEARCH_PATH: DWORD = DWORD;
pub(crate) const LOAD_LIBRARY_REQUIRE_SIGNED_TARGET: DWORD = DWORD;
pub(crate) const LOAD_LIBRARY_SAFE_CURRENT_DIRS: DWORD = DWORD;
use super::LOAD_LIBRARY_FLAGS;
pub(crate) const LOAD_IGNORE_CODE_AUTHZ_LEVEL: LOAD_LIBRARY_FLAGS = LOAD_LIBRARY_FLAGS;
pub(crate) const LOAD_LIBRARY_AS_DATAFILE: LOAD_LIBRARY_FLAGS = LOAD_LIBRARY_FLAGS;
pub(crate) const LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE: LOAD_LIBRARY_FLAGS = LOAD_LIBRARY_FLAGS;
pub(crate) const LOAD_LIBRARY_AS_IMAGE_RESOURCE: LOAD_LIBRARY_FLAGS = LOAD_LIBRARY_FLAGS;
pub(crate) const LOAD_LIBRARY_SEARCH_APPLICATION_DIR: LOAD_LIBRARY_FLAGS = LOAD_LIBRARY_FLAGS;
pub(crate) const LOAD_LIBRARY_SEARCH_DEFAULT_DIRS: LOAD_LIBRARY_FLAGS = LOAD_LIBRARY_FLAGS;
pub(crate) const LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR: LOAD_LIBRARY_FLAGS = LOAD_LIBRARY_FLAGS;
pub(crate) const LOAD_LIBRARY_SEARCH_SYSTEM32: LOAD_LIBRARY_FLAGS = LOAD_LIBRARY_FLAGS;
pub(crate) const LOAD_LIBRARY_SEARCH_USER_DIRS: LOAD_LIBRARY_FLAGS = LOAD_LIBRARY_FLAGS;
pub(crate) const LOAD_WITH_ALTERED_SEARCH_PATH: LOAD_LIBRARY_FLAGS = LOAD_LIBRARY_FLAGS;
pub(crate) const LOAD_LIBRARY_REQUIRE_SIGNED_TARGET: LOAD_LIBRARY_FLAGS = LOAD_LIBRARY_FLAGS;
pub(crate) const LOAD_LIBRARY_SAFE_CURRENT_DIRS: LOAD_LIBRARY_FLAGS = LOAD_LIBRARY_FLAGS;
}
}
#[cfg(any(not(libloading_docs), windows))]
mod windows_imports {
extern crate winapi;
pub(super) use self::winapi::shared::minwindef::{WORD, DWORD, HMODULE, FARPROC};
pub(super) use self::winapi::shared::ntdef::WCHAR;
pub(super) use self::winapi::um::{errhandlingapi, libloaderapi};
extern crate windows_sys;
pub(super) use self::windows_sys::Win32::Foundation::{GetLastError, FARPROC, HMODULE};
pub(super) use self::windows_sys::Win32::System::Diagnostics::Debug as debug_api;
pub(super) use self::windows_sys::Win32::System::Diagnostics::Debug::SEM_FAILCRITICALERRORS;
pub(super) use self::windows_sys::Win32::System::Diagnostics::Debug::THREAD_ERROR_MODE;

pub(super) use self::windows_sys::Win32::System::LibraryLoader as library_loader;
pub(super) use self::windows_sys::Win32::System::LibraryLoader::LOAD_LIBRARY_FLAGS;

pub(super) use std::os::windows::ffi::{OsStrExt, OsStringExt};
pub(super) const SEM_FAILCE: DWORD = 1;

pub(super) mod consts {
pub(crate) use super::winapi::um::libloaderapi::{
pub(crate) use super::windows_sys::Win32::System::LibraryLoader::{
LOAD_IGNORE_CODE_AUTHZ_LEVEL,
LOAD_LIBRARY_AS_DATAFILE,
LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE,
Expand Down Expand Up @@ -116,9 +122,9 @@ impl Library {
/// [MSDN]: https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-getmodulehandleexw
pub fn this() -> Result<Library, crate::Error> {
unsafe {
let mut handle: HMODULE = std::ptr::null_mut();
let mut handle: HMODULE = 0;
with_get_last_error(|source| crate::Error::GetModuleHandleExW { source }, || {
let result = libloaderapi::GetModuleHandleExW(0, std::ptr::null_mut(), &mut handle);
let result = library_loader::GetModuleHandleExW(0, std::ptr::null_mut(), &mut handle);
if result == 0 {
None
} else {
Expand Down Expand Up @@ -149,11 +155,11 @@ impl Library {
let wide_filename: Vec<u16> = filename.as_ref().encode_wide().chain(Some(0)).collect();

let ret = unsafe {
let mut handle: HMODULE = std::ptr::null_mut();
let mut handle: HMODULE = 0;
with_get_last_error(|source| crate::Error::GetModuleHandleExW { source }, || {
// Make sure no winapi calls as a result of drop happen inside this closure, because
// otherwise that might change the return value of the GetLastError.
let result = libloaderapi::GetModuleHandleExW(0, wide_filename.as_ptr(), &mut handle);
let result = library_loader::GetModuleHandleExW(0, wide_filename.as_ptr(), &mut handle);
if result == 0 {
None
} else {
Expand Down Expand Up @@ -186,16 +192,15 @@ impl Library {
/// Additionally, the callers of this function must also ensure that execution of the
/// termination routines contained within the library is safe as well. These routines may be
/// executed when the library is unloaded.
pub unsafe fn load_with_flags<P: AsRef<OsStr>>(filename: P, flags: DWORD) -> Result<Library, crate::Error> {
pub unsafe fn load_with_flags<P: AsRef<OsStr>>(filename: P, flags: LOAD_LIBRARY_FLAGS) -> Result<Library, crate::Error> {
let wide_filename: Vec<u16> = filename.as_ref().encode_wide().chain(Some(0)).collect();
let _guard = ErrorModeGuard::new();

let ret = with_get_last_error(|source| crate::Error::LoadLibraryExW { source }, || {
// Make sure no winapi calls as a result of drop happen inside this closure, because
// otherwise that might change the return value of the GetLastError.
let handle =
libloaderapi::LoadLibraryExW(wide_filename.as_ptr(), std::ptr::null_mut(), flags);
if handle.is_null() {
let handle = library_loader::LoadLibraryExW(wide_filename.as_ptr(), 0, flags);
if handle == 0 {
None
} else {
Some(Library(handle))
Expand All @@ -221,8 +226,8 @@ impl Library {
ensure_compatible_types::<T, FARPROC>()?;
let symbol = cstr_cow_from_bytes(symbol)?;
with_get_last_error(|source| crate::Error::GetProcAddress { source }, || {
let symbol = libloaderapi::GetProcAddress(self.0, symbol.as_ptr());
if symbol.is_null() {
let symbol = library_loader::GetProcAddress(self.0, symbol.as_ptr().cast());
if symbol.is_none() {
None
} else {
Some(Symbol {
Expand All @@ -238,12 +243,12 @@ impl Library {
/// # Safety
///
/// Users of this API must specify the correct type of the function or variable loaded.
pub unsafe fn get_ordinal<T>(&self, ordinal: WORD) -> Result<Symbol<T>, crate::Error> {
pub unsafe fn get_ordinal<T>(&self, ordinal: u16) -> Result<Symbol<T>, crate::Error> {
ensure_compatible_types::<T, FARPROC>()?;
with_get_last_error(|source| crate::Error::GetProcAddress { source }, || {
let ordinal = ordinal as usize as *mut _;
let symbol = libloaderapi::GetProcAddress(self.0, ordinal);
if symbol.is_null() {
let ordinal = ordinal as usize as *const _;
let symbol = library_loader::GetProcAddress(self.0, ordinal);
if symbol.is_none() {
None
} else {
Some(Symbol {
Expand Down Expand Up @@ -280,7 +285,7 @@ impl Library {
/// The underlying data structures may still get leaked if an error does occur.
pub fn close(self) -> Result<(), crate::Error> {
let result = with_get_last_error(|source| crate::Error::FreeLibrary { source }, || {
if unsafe { libloaderapi::FreeLibrary(self.0) == 0 } {
if unsafe { library_loader::FreeLibrary(self.0) == 0 } {
None
} else {
Some(())
Expand All @@ -296,7 +301,7 @@ impl Library {

impl Drop for Library {
fn drop(&mut self) {
unsafe { libloaderapi::FreeLibrary(self.0); }
unsafe { library_loader::FreeLibrary(self.0); }
}
}

Expand All @@ -305,17 +310,17 @@ impl fmt::Debug for Library {
unsafe {
// FIXME: use Maybeuninit::uninit_array when stable
let mut buf =
mem::MaybeUninit::<[mem::MaybeUninit::<WCHAR>; 1024]>::uninit().assume_init();
let len = libloaderapi::GetModuleFileNameW(self.0,
mem::MaybeUninit::<[mem::MaybeUninit<u16>; 1024]>::uninit().assume_init();
let len = library_loader::GetModuleFileNameW(self.0,
buf[..].as_mut_ptr().cast(), 1024) as usize;
if len == 0 {
f.write_str(&format!("Library@{:p}", self.0))
f.write_str(&format!("Library@{:#x}", self.0))
} else {
let string: OsString = OsString::from_wide(
// FIXME: use Maybeuninit::slice_get_ref when stable
&*(&buf[..len] as *const [_] as *const [WCHAR])
&*(&buf[..len] as *const [_] as *const [u16]),
);
f.write_str(&format!("Library@{:p} from {:?}", self.0, string))
f.write_str(&format!("Library@{:#x} from {:?}", self.0, string))
}
}
}
Expand All @@ -340,7 +345,7 @@ impl<T> Symbol<T> {
impl<T> Symbol<Option<T>> {
/// Lift Option out of the symbol.
pub fn lift_option(self) -> Option<Symbol<T>> {
if self.pointer.is_null() {
if self.pointer.is_none() {
None
} else {
Some(Symbol {
Expand All @@ -363,33 +368,33 @@ impl<T> Clone for Symbol<T> {
impl<T> ::std::ops::Deref for Symbol<T> {
type Target = T;
fn deref(&self) -> &T {
unsafe {
// Additional reference level for a dereference on `deref` return value.
&*(&self.pointer as *const *mut _ as *const T)
}
unsafe { &*((&self.pointer) as *const FARPROC as *const T) }
}
}

impl<T> fmt::Debug for Symbol<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&format!("Symbol@{:p}", self.pointer))
match self.pointer {
None => f.write_str("Symbol@0x0"),
Some(ptr) => f.write_str(&format!("Symbol@{:p}", ptr as *const ())),
}
}
}

struct ErrorModeGuard(DWORD);
struct ErrorModeGuard(THREAD_ERROR_MODE);

impl ErrorModeGuard {
#[allow(clippy::if_same_then_else)]
fn new() -> Option<ErrorModeGuard> {
unsafe {
let mut previous_mode = 0;
if errhandlingapi::SetThreadErrorMode(SEM_FAILCE, &mut previous_mode) == 0 {
if debug_api::SetThreadErrorMode(SEM_FAILCRITICALERRORS, &mut previous_mode) == 0 {
// How in the world is it possible for what is essentially a simple variable swap
// to fail? For now we just ignore the error -- the worst that can happen here is
// the previous mode staying on and user seeing a dialog error on older Windows
// machines.
None
} else if previous_mode == SEM_FAILCE {
} else if previous_mode == SEM_FAILCRITICALERRORS {
None
} else {
Some(ErrorModeGuard(previous_mode))
Expand All @@ -401,7 +406,7 @@ impl ErrorModeGuard {
impl Drop for ErrorModeGuard {
fn drop(&mut self) {
unsafe {
errhandlingapi::SetThreadErrorMode(self.0, ptr::null_mut());
debug_api::SetThreadErrorMode(self.0, ptr::null_mut());
}
}
}
Expand All @@ -410,7 +415,7 @@ fn with_get_last_error<T, F>(wrap: fn(crate::error::WindowsError) -> crate::Erro
-> Result<T, Option<crate::Error>>
where F: FnOnce() -> Option<T> {
closure().ok_or_else(|| {
let error = unsafe { errhandlingapi::GetLastError() };
let error = unsafe { GetLastError() };
if error == 0 {
None
} else {
Expand All @@ -425,7 +430,7 @@ where F: FnOnce() -> Option<T> {
/// recommended for use in setup programs that must run extracted DLLs during installation.
///
/// See [flag documentation on MSDN](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexw#parameters).
pub const LOAD_IGNORE_CODE_AUTHZ_LEVEL: DWORD = consts::LOAD_IGNORE_CODE_AUTHZ_LEVEL;
pub const LOAD_IGNORE_CODE_AUTHZ_LEVEL: LOAD_LIBRARY_FLAGS = consts::LOAD_IGNORE_CODE_AUTHZ_LEVEL;

/// Map the file into the calling process’ virtual address space as if it were a data file.
///
Expand All @@ -435,7 +440,7 @@ pub const LOAD_IGNORE_CODE_AUTHZ_LEVEL: DWORD = consts::LOAD_IGNORE_CODE_AUTHZ_L
/// messages or resources from it.
///
/// See [flag documentation on MSDN](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexw#parameters).
pub const LOAD_LIBRARY_AS_DATAFILE: DWORD = consts::LOAD_LIBRARY_AS_DATAFILE;
pub const LOAD_LIBRARY_AS_DATAFILE: LOAD_LIBRARY_FLAGS = consts::LOAD_LIBRARY_AS_DATAFILE;

/// Map the file into the calling process’ virtual address space as if it were a data file.
///
Expand All @@ -444,7 +449,7 @@ pub const LOAD_LIBRARY_AS_DATAFILE: DWORD = consts::LOAD_LIBRARY_AS_DATAFILE;
/// while it is in use. However, the DLL can still be opened by other processes.
///
/// See [flag documentation on MSDN](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexw#parameters).
pub const LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE: DWORD = consts::LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE;
pub const LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE: LOAD_LIBRARY_FLAGS = consts::LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE;

/// Map the file into the process’ virtual address space as an image file.
///
Expand All @@ -456,15 +461,15 @@ pub const LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE: DWORD = consts::LOAD_LIBRARY_AS_DA
/// [`LOAD_LIBRARY_AS_DATAFILE`].
///
/// See [flag documentation on MSDN](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexw#parameters).
pub const LOAD_LIBRARY_AS_IMAGE_RESOURCE: DWORD = consts::LOAD_LIBRARY_AS_IMAGE_RESOURCE;
pub const LOAD_LIBRARY_AS_IMAGE_RESOURCE: LOAD_LIBRARY_FLAGS = consts::LOAD_LIBRARY_AS_IMAGE_RESOURCE;

/// Search the application's installation directory for the DLL and its dependencies.
///
/// Directories in the standard search path are not searched. This value cannot be combined with
/// [`LOAD_WITH_ALTERED_SEARCH_PATH`].
///
/// See [flag documentation on MSDN](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexw#parameters).
pub const LOAD_LIBRARY_SEARCH_APPLICATION_DIR: DWORD = consts::LOAD_LIBRARY_SEARCH_APPLICATION_DIR;
pub const LOAD_LIBRARY_SEARCH_APPLICATION_DIR: LOAD_LIBRARY_FLAGS = consts::LOAD_LIBRARY_SEARCH_APPLICATION_DIR;

/// Search default directories when looking for the DLL and its dependencies.
///
Expand All @@ -474,7 +479,7 @@ pub const LOAD_LIBRARY_SEARCH_APPLICATION_DIR: DWORD = consts::LOAD_LIBRARY_SEAR
/// [`LOAD_WITH_ALTERED_SEARCH_PATH`].
///
/// See [flag documentation on MSDN](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexw#parameters).
pub const LOAD_LIBRARY_SEARCH_DEFAULT_DIRS: DWORD = consts::LOAD_LIBRARY_SEARCH_DEFAULT_DIRS;
pub const LOAD_LIBRARY_SEARCH_DEFAULT_DIRS: LOAD_LIBRARY_FLAGS = consts::LOAD_LIBRARY_SEARCH_DEFAULT_DIRS;

/// Directory that contains the DLL is temporarily added to the beginning of the list of
/// directories that are searched for the DLL’s dependencies.
Expand All @@ -485,15 +490,15 @@ pub const LOAD_LIBRARY_SEARCH_DEFAULT_DIRS: DWORD = consts::LOAD_LIBRARY_SEARCH_
/// with [`LOAD_WITH_ALTERED_SEARCH_PATH`].
///
/// See [flag documentation on MSDN](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexw#parameters).
pub const LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR: DWORD = consts::LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR;
pub const LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR: LOAD_LIBRARY_FLAGS = consts::LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR;

/// Search `%windows%\system32` for the DLL and its dependencies.
///
/// Directories in the standard search path are not searched. This value cannot be combined with
/// [`LOAD_WITH_ALTERED_SEARCH_PATH`].
///
/// See [flag documentation on MSDN](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexw#parameters).
pub const LOAD_LIBRARY_SEARCH_SYSTEM32: DWORD = consts::LOAD_LIBRARY_SEARCH_SYSTEM32;
pub const LOAD_LIBRARY_SEARCH_SYSTEM32: LOAD_LIBRARY_FLAGS = consts::LOAD_LIBRARY_SEARCH_SYSTEM32;

/// Directories added using the `AddDllDirectory` or the `SetDllDirectory` function are searched
/// for the DLL and its dependencies.
Expand All @@ -503,7 +508,7 @@ pub const LOAD_LIBRARY_SEARCH_SYSTEM32: DWORD = consts::LOAD_LIBRARY_SEARCH_SYST
/// combined with [`LOAD_WITH_ALTERED_SEARCH_PATH`].
///
/// See [flag documentation on MSDN](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexw#parameters).
pub const LOAD_LIBRARY_SEARCH_USER_DIRS: DWORD = consts::LOAD_LIBRARY_SEARCH_USER_DIRS;
pub const LOAD_LIBRARY_SEARCH_USER_DIRS: LOAD_LIBRARY_FLAGS = consts::LOAD_LIBRARY_SEARCH_USER_DIRS;

/// If `filename` specifies an absolute path, the system uses the alternate file search strategy
/// discussed in the [Remarks section] to find associated executable modules that the specified
Expand All @@ -518,15 +523,15 @@ pub const LOAD_LIBRARY_SEARCH_USER_DIRS: DWORD = consts::LOAD_LIBRARY_SEARCH_USE
/// See [flag documentation on MSDN](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexw#parameters).
///
/// [Remarks]: https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexw#remarks
pub const LOAD_WITH_ALTERED_SEARCH_PATH: DWORD = consts::LOAD_WITH_ALTERED_SEARCH_PATH;
pub const LOAD_WITH_ALTERED_SEARCH_PATH: LOAD_LIBRARY_FLAGS = consts::LOAD_WITH_ALTERED_SEARCH_PATH;

/// Specifies that the digital signature of the binary image must be checked at load time.
///
/// See [flag documentation on MSDN](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexw#parameters).
pub const LOAD_LIBRARY_REQUIRE_SIGNED_TARGET: DWORD = consts::LOAD_LIBRARY_REQUIRE_SIGNED_TARGET;
pub const LOAD_LIBRARY_REQUIRE_SIGNED_TARGET: LOAD_LIBRARY_FLAGS = consts::LOAD_LIBRARY_REQUIRE_SIGNED_TARGET;

/// Allow loading a DLL for execution from the current directory only if it is under a directory in
/// the Safe load list.
///
/// See [flag documentation on MSDN](https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-loadlibraryexw#parameters).
pub const LOAD_LIBRARY_SAFE_CURRENT_DIRS: DWORD = consts::LOAD_LIBRARY_SAFE_CURRENT_DIRS;
pub const LOAD_LIBRARY_SAFE_CURRENT_DIRS: LOAD_LIBRARY_FLAGS = consts::LOAD_LIBRARY_SAFE_CURRENT_DIRS;
Loading

0 comments on commit c7955a7

Please sign in to comment.