diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml new file mode 100644 index 0000000..1e30bd0 --- /dev/null +++ b/.github/workflows/windows-ci.yml @@ -0,0 +1,63 @@ +name: Windows CI + +on: + push: + branches: + - master + pull_request: + release: + types: + - published + +jobs: + check-and-test: + if: ${{ github.event_name != 'release' }} + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Cargo check (all targets) + run: cargo check --workspace --all-targets + + - name: Cargo check (examples) + if: ${{ always() }} + run: cargo check --workspace --examples + + - name: Cargo fmt (check) + if: ${{ always() }} + run: cargo fmt --all -- --check + + - name: Cargo clippy + if: ${{ always() }} + run: cargo clippy --workspace --all-targets -- -D warnings + + - name: Cargo test + if: ${{ always() }} + run: cargo test --workspace --all-targets + + - name: Cargo publish (dry run) + if: ${{ always() }} + run: cargo publish --dry-run + + publish: + if: ${{ github.event_name == 'release' && github.event.action == 'published' }} + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cargo publish + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish diff --git a/Cargo.toml b/Cargo.toml index 896df82..65c33a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "wasapi" -version = "0.22.0" +version = "0.23.0" edition = "2021" -rust-version = "1.74" +rust-version = "1.76" authors = ["HEnquist "] description = "Bindings for the Wasapi API on Windows" license = "MIT" @@ -29,15 +29,15 @@ features = ["Foundation", "Win32_Security",] [dependencies] -log = "0.4.22" +log = "0.4" num-integer = "0.1" windows-core = "0.62" -thiserror = "2.0.9" +thiserror = "2.0" [dev-dependencies] -simplelog = "0.12.2" -rand = "0.9.1" -sysinfo = "0.37.1" +simplelog = "0.12" +rand = "0.10" +sysinfo = "0.38" [package.metadata.docs.rs] default-target = "x86_64-pc-windows-msvc" diff --git a/src/api.rs b/src/api.rs index 7c2001c..53f08fc 100644 --- a/src/api.rs +++ b/src/api.rs @@ -531,10 +531,10 @@ impl Device { /// that flows between the audio engine and the audio endpoint device when the device operates in shared mode. pub fn get_device_format(&self) -> WasapiRes { let data = self.get_blob_property(&PKEY_AudioEngine_DeviceFormat)?; - // SAFETY: PKEY_AudioEngine_DeviceFormat is guaranteed to be a WAVEFORMATEX structure based on MSFT docs: + // PKEY_AudioEngine_DeviceFormat contains a WAVEFORMATEX/WAVEFORMATEXTENSIBLE blob: // https://learn.microsoft.com/en-us/windows/win32/coreaudio/pkey-audioengine-deviceformat - let waveformatex: &WAVEFORMATEX = unsafe { &*(data.as_ptr() as *const _) }; - WaveFormat::parse(waveformatex) + // The blob can be byte-aligned only, so parse without creating aligned references. + WaveFormat::parse_from_blob_bytes(&data) } /// Read a string property from an [IMMDevice] @@ -564,7 +564,7 @@ impl Device { fn parse_string_property(prop: &PROPVARIANT) -> WasapiRes { let propstr = unsafe { PropVariantToStringAlloc(prop)? }; let name = unsafe { propstr.to_string()? }; - unsafe { CoTaskMemFree(Some(propstr.0 as _)) }; + unsafe { CoTaskMemFree(Some(propstr.0.cast())) }; trace!("name: {name}"); Ok(name) } @@ -585,7 +585,7 @@ impl Device { let idstr = unsafe { self.device.GetId()? }; //let wide_id = unsafe { U16CString::from_ptr_str(idstr.0) }; let id = unsafe { idstr.to_string()? }; - unsafe { CoTaskMemFree(Some(idstr.0 as _)) }; + unsafe { CoTaskMemFree(Some(idstr.0.cast())) }; //let id = wide_id.to_string_lossy(); trace!("id: {id}"); Ok(id) @@ -706,7 +706,7 @@ impl AudioClient { Anonymous: PROPVARIANT_0_0_0 { blob: BLOB { cbSize: size_of::() as u32, - pBlobData: pinned_params.get_mut() as *const _ as *mut _, + pBlobData: std::ptr::from_mut(pinned_params.get_mut()).cast(), }, }, }), @@ -715,7 +715,7 @@ impl AudioClient { let activation_prop = ManuallyDrop::new(raw_prop); let pinned_prop = Pin::new(activation_prop.deref()); - let activation_params = Some(pinned_prop.get_ref() as *const _); + let activation_params = Some(std::ptr::from_ref(pinned_prop.get_ref())); // Create completion handler let setup = Arc::new((Mutex::new(false), Condvar::new())); @@ -767,7 +767,7 @@ impl AudioClient { if temp_fmt.cbSize == 22 && temp_fmt.wFormatTag as u32 == WAVE_FORMAT_EXTENSIBLE { let format = unsafe { WaveFormat { - wave_fmt: (temp_fmt_ptr as *const _ as *const WAVEFORMATEXTENSIBLE).read(), + wave_fmt: temp_fmt_ptr.cast::().read(), } }; @@ -840,9 +840,8 @@ impl AudioClient { && temp_fmt.wFormatTag as u32 == WAVE_FORMAT_EXTENSIBLE { debug!("got the nearest matching format as a WAVEFORMATEXTENSIBLE"); - let temp_fmt_ext: WAVEFORMATEXTENSIBLE = unsafe { - (supported_format as *const _ as *const WAVEFORMATEXTENSIBLE).read() - }; + let temp_fmt_ext: WAVEFORMATEXTENSIBLE = + unsafe { supported_format.cast::().read() }; unsafe { CoTaskMemFree(Some(supported_format.cast())) }; @@ -1863,16 +1862,21 @@ impl AudioEffectsManager { .GetAudioEffects(&mut audio_effects, &mut num_effects)?; } - if num_effects > 0 { + let effects = if num_effects > 0 { let effects_slice = unsafe { slice::from_raw_parts(audio_effects, num_effects as usize) }; let effects_vec = effects_slice.to_vec(); - // Free the memory allocated for the audio effects. - unsafe { CoTaskMemFree(Some(audio_effects as *mut _)) }; - Ok(Some(effects_vec)) + Some(effects_vec) } else { - Ok(None) + None + }; + + if !audio_effects.is_null() { + // Free the memory allocated for the audio effects. + unsafe { CoTaskMemFree(Some(audio_effects.cast())) }; } + + Ok(effects) } } diff --git a/src/waveformat.rs b/src/waveformat.rs index 29aa49f..79dd82e 100644 --- a/src/waveformat.rs +++ b/src/waveformat.rs @@ -89,6 +89,34 @@ impl fmt::Debug for WaveFormat { } impl WaveFormat { + /// Parse a potentially unaligned byte slice containing a WAVEFORMATEX or WAVEFORMATEXTENSIBLE. + pub fn parse_from_blob_bytes(blob: &[u8]) -> WasapiRes { + if blob.len() < size_of::() { + return Err(WasapiError::UnsupportedFormat); + } + + let waveformatex: WAVEFORMATEX = unsafe { std::ptr::read_unaligned(blob.as_ptr().cast()) }; + + if waveformatex.wFormatTag == WAVE_FORMAT_EXTENSIBLE as u16 { + const ATLEAST_SIZE: usize = + size_of::() - size_of::(); + if waveformatex.cbSize < ATLEAST_SIZE as u16 { + return Err(WasapiError::UnsupportedFormat); + } + + let declared_size = size_of::() + waveformatex.cbSize as usize; + if blob.len() < declared_size { + return Err(WasapiError::UnsupportedFormat); + } + + let waveformatextensible: WAVEFORMATEXTENSIBLE = + unsafe { std::ptr::read_unaligned(blob.as_ptr().cast()) }; + return Ok(waveformatextensible.into()); + } + + Self::from_waveformatex(waveformatex) + } + /// Parse a [WAVEFORMATEX](https://docs.microsoft.com/en-us/previous-versions/dd757713(v=vs.85)) structure and /// return a [WaveFormat] instance. If the underlying structure is a WAVEFORMATEXTENSIBLE, as specified by /// wFormatTag, then use as-is. If not, assume it is only a WAVEFORMATEX structure. @@ -101,7 +129,7 @@ impl WaveFormat { } // SAFETY: Both wFormatTag and size check passed, so the pointed-to memory is a full WAVEFORMATEXTENSIBLE. let waveformatextensible: WAVEFORMATEXTENSIBLE = unsafe { - std::ptr::read(waveformatex as *const WAVEFORMATEX as *const WAVEFORMATEXTENSIBLE) + std::ptr::read(std::ptr::from_ref(waveformatex).cast::()) }; return Ok(waveformatextensible.into()); }