動機
Thumbs.db の中のサムネイル画像を抽出してみたくなったので調べました。
環境
- Windows 11 25H2
- rustc 1.96.0
実装
Thumbs.db は OLE 構造化ストレージ という形式(古い Word とか Excel で使われていたファイル形式と同じ)なので StgOpenStorage という WinAPI で開けるそうです。IStorage インターフェースを取得できたら EnumElements メソッドでストリームを列挙して、IStream (正確には ISequentialStream)の Read メソッドでバッファにデータを読み込みます。読み込んだデータから JPEG の SOI と EOI を探してファイルに保存しておしまい。意外に簡単なので、コードを全部書きます。
Cargo.toml
[package]
name = "thumbs-db"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "=1.0.102"
[dependencies.windows]
version = "=0.62.2"
features = [
"Win32_System_Com",
"Win32_System_Com_StructuredStorage",
]
main.rs
use std::{env, fs, ops::Deref};
use anyhow::{Context, Result};
use windows::{
Win32::System::Com::{
CoInitialize, CoTaskMemFree, CoUninitialize, STATSTG, STGM_READ, STGM_SHARE_DENY_WRITE,
STGM_SHARE_EXCLUSIVE, STGTY_STREAM, StructuredStorage::StgOpenStorage,
},
core::HSTRING,
};
// COM の初期化と終了処理を実行するための構造体
struct Com;
impl Com {
fn new() -> Result<Self> {
unsafe { CoInitialize(None).ok()? };
Ok(Self)
}
}
impl Drop for Com {
fn drop(&mut self) {
unsafe { CoUninitialize() };
}
}
// 生の [STATSTG] のままだと使いにくいのでラップしてメソッドを生やすための構造体
struct Stat([STATSTG; 1]);
impl Stat {
fn new() -> Self {
Self([STATSTG::default()])
}
fn as_mut_slice(&mut self) -> &mut [STATSTG] {
&mut self.0
}
fn name(&self) -> Result<String> {
Ok(unsafe { self.pwcsName.to_string() }?)
}
}
impl Deref for Stat {
type Target = STATSTG;
fn deref(&self) -> &Self::Target {
&self.0[0]
}
}
// pwcsName のメモリ領域を解放
impl Drop for Stat {
fn drop(&mut self) {
unsafe { CoTaskMemFree(Some(self.pwcsName.0 as _)) };
}
}
fn main() -> Result<()> {
// impl From<String> for HSTRING が実装されているので into() で HSTRING に変換できる
let arg: HSTRING = env::args().nth(1).context("no args")?.into();
// COM の初期化処理
let _com = Com::new()?;
// ストレージを開く
let storage =
unsafe { StgOpenStorage(&arg, None, STGM_READ | STGM_SHARE_DENY_WRITE, None, 0)? };
// すべてのストリームを列挙する
let enm = unsafe { storage.EnumElements(None, None, None)? };
loop {
let mut stat = Stat::new();
// 次のストリームへ移動
unsafe { enm.Next(stat.as_mut_slice(), Some(&mut 0))? };
// Next 関数が常に Ok(()) が帰すのでサイズ 0 だったら処理を終了
if stat.cbSize == 0 {
break;
}
// ストリームのときだけ処理。それ以外はスルー
if stat.r#type != STGTY_STREAM.0 as _ {
continue;
}
// ストリームを開く
let stream = unsafe {
storage.OpenStream(stat.pwcsName, None, STGM_READ | STGM_SHARE_EXCLUSIVE, 0)?
};
// ストリームを書き込むバッファを確保
let mut buf = vec![0u8; stat.cbSize as _];
// ストリームの内容をバッファに読み込む
unsafe {
stream
.Read(buf.as_mut_ptr() as _, stat.cbSize as _, Some(&mut 0))
.ok()?
};
// JPEG の SOI と EOI を探す
let mut win = buf.windows(2);
let start = win
.position(|v| v.eq(&[0xff, 0xd8]))
.context("soi not found")?;
let end = win
.position(|v| v.eq(&[0xff, 0xd9]))
.map(|v| v + 2) // EOI の 2bytes 分追加
.context("eoi not found")?;
// JPEG としてファイルに保存する
fs::write(format!("{}.jpg", stat.name()?), &buf[start..end])?;
}
Ok(())
// 自動的に COM の終了処理が実行される
}
使用例
cargo run -- /path/to/Thumbs.db
私の環境では256_2b1e89589ce071a7.jpg(画像サイズ_GUID?.jpg)のようなファイル名で抽出できました。