ファイルのプロパティを表示する
動機
↓ファイルのプロパティで出てくるコレをプログラムで取得したかったので調べました。

環境
Windows 25H2
rustc 1.92.0
実装
COM の初期化
ファイルのプロパティを取得するにはIPropertyStoreインターフェースを使用します。IPropertyStoreインターフェースを使うには COM を使用するので、まず COM を初期化します。自動的に終了処理が走るようにimpl Dropも書いておきます。
struct Com;
impl Com {
fn new() -> Result<Self> {
unsafe { CoInitialize(None).ok()? };
Ok(Self)
}
}
impl Drop for Com {
fn drop(&mut self) {
unsafe { CoUninitialize() };
}
}
fn main() -> Result<()> {
let _com = Com::new()?;
Ok(())
}
IPropertyStore
IPropertyStoreインターフェースを取得するにはSHGetPropertyStoreFromParsingName関数を呼べば良いようです。第 1 引数にファイルパスを渡します。第 3 引数はGPS_DEFAULT(0)にもできるようですが、詳細な情報を得るにはGPS_OPENSLOWITEMを指定する方が良いようです?(あとで調べる)。
let store: IPropertyStore =
unsafe { SHGetPropertyStoreFromParsingName(&path, None, GPS_OPENSLOWITEM)? };
IPropetyStoreインターフェースにはGetAtメソッドやGetValueメソッドが定義されているので、これらのメソッドを呼べばプロパティの値を取得できるようです。そのままでは使いにくいので Rust の構造体でラップしてIteratorも実装します。
struct PropertyStore<'a>(&'a IPropertyStore);
impl<'a> PropertyStore<'a> {
fn new(store: &'a IPropertyStore) -> Self {
Self(store)
}
// プロパティの数を返す
fn get_count(&self) -> Result<u32> {
Ok(unsafe { self.0.GetCount()? })
}
// index の場所のプロパティキー("撮影日"とか"画像の幅"とかの GUID)を返す
fn get_at(&self, index: u32) -> Result<PROPERTYKEY> {
let mut key = PROPERTYKEY::default();
unsafe { self.0.GetAt(index, &mut key)? };
Ok(key)
}
// プロパティキーの値("2025/12/25" とか "2048")を返す
fn get_value(&self, key: &PROPERTYKEY) -> Result<PROPVARIANT> {
Ok(unsafe { self.0.GetValue(key)? })
}
// プロパティキー・値タプルのイテレータを返す
fn key_values(&self) -> impl Iterator<Item = (PROPERTYKEY, PROPVARIANT)> {
PropertyStoreIterator {
store: self,
current: 0,
}
}
}
// イテレータを実装するための構造体
struct PropertyStoreIterator<'a> {
store: &'a PropertyStore<'a>,
current: u32,
}
// イテレータの実装
impl<'a> Iterator for PropertyStoreIterator<'a> {
type Item = (PROPERTYKEY, PROPVARIANT);
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.store.get_count().ok()? {
let key = self.store.get_at(self.current).ok()?;
let value = self.store.get_value(&key).ok()?;
self.current += 1;
return Some((key, value));
}
None
}
}
IPropertySystem
前述の通りIteratorを実装したことでPROPERTYKEYとPROPVARIANTを取得できるようになりました。PROPERTYKEYはfmtid: GUIDとpid: u32をメンバに持つ構造体です。PROPERTYKEY構造体そのままでは何のプロパティを示しているのか人間にはわからないので、人間にわかりやすい文字列に変換します。その変換をするためにIPropertySystemインターフェースを使用します。
// CLSID_PropertySystem は windows クレートで定義されていないので自分で定義する
const CLSID_PropertySystem: GUID = GUID {
data1: 0xb8967f85,
data2: 0x58ae,
data3: 0x4f46,
data4: [0x9f, 0xb2, 0x5d, 0x79, 0x04, 0x79, 0x8f, 0x4b],
};
// IPropertySystem インターフェースの取得
let prop_system: IPropertySystem =
unsafe { CoCreateInstance(&CLSID_PropertySystem, None, CLSCTX_INPROC_SERVER)? };
// PROPERYKEY 構造体から IPropertyDescription インターフェースを取得
let prop_desc: IPropertyDescription =
unsafe { prop_system.GetPropertyDescription(&k).ok()? };
// 人間にわかりやすいプロパティ名を取得(例えば、"画像の幅"とか"ビットレート"とか)
let key = unsafe { prop_desc.GetDisplayName().ok()?.to_string().ok()? };
次はPROPVARIANTです。PROPVARIANTにはDisplayトレイトが実装されているようなので(windows クレートのソースを見たけど、どこで実装されてるのかわからんかった。あとで確認する StructuredStorage.rs の 61 行目!)、とりあえずto_string()メソッドを呼べば文字列として表示できます。ただし、時刻表示が GMT だったり、ファイルサイズがバイト単位だったりして人間にはわかりにくいので、IPropertySystemのFormatForDisplayAllocメソッドを使用します。そうすると時刻の GMT が JST に変換されたり、ファイルサイズが435616から425 KBという表示に変換されてわかりやすくなります。
let value = unsafe {
prop_system
.FormatForDisplayAlloc(&k, &v, PDFF_DEFAULT) // PROPERTYKEY と PROPVARIANT を引数にとる
.ok()?
.to_string() // Rust の文字列に変換
.ok()?
.replace(r"\", r"\\") // エスケープ処理
};
結果
最終的に↓のようになりました。取得したプロパティを JSON にして nushell に渡すことでいい感じにテーブル表示しています。

まとめ
コピペ用サンプルコード
[package]
name = "property"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0"
[dependencies.windows]
version = "0.62"
features = [
"Win32_Storage_FileSystem",
"Win32_System_Com",
"Win32_System_Variant",
"Win32_UI_Shell_PropertiesSystem",
"Win32_System_Com_StructuredStorage",
]
use anyhow::{Context, Result, ensure};
use std::env;
use windows::{
Win32::{
Foundation::PROPERTYKEY,
Storage::FileSystem::GetFullPathNameW,
System::Com::{
CLSCTX_INPROC_SERVER, CoCreateInstance, CoInitialize, CoUninitialize,
StructuredStorage::PROPVARIANT,
},
UI::Shell::PropertiesSystem::{
GPS_OPENSLOWITEM, IPropertyDescription, IPropertyStore, IPropertySystem, PDFF_DEFAULT,
SHGetPropertyStoreFromParsingName,
},
},
core::{GUID, HSTRING, PWSTR},
};
#[allow(non_upper_case_globals)]
const CLSID_PropertySystem: GUID = GUID {
data1: 0xb8967f85,
data2: 0x58ae,
data3: 0x4f46,
data4: [0x9f, 0xb2, 0x5d, 0x79, 0x04, 0x79, 0x8f, 0x4b],
};
struct Com;
impl Com {
fn new() -> Result<Self> {
unsafe { CoInitialize(None).ok()? };
Ok(Self)
}
}
impl Drop for Com {
fn drop(&mut self) {
unsafe { CoUninitialize() };
}
}
struct PropertyStore<'a>(&'a IPropertyStore);
impl<'a> PropertyStore<'a> {
fn new(store: &'a IPropertyStore) -> Self {
Self(store)
}
fn get_count(&self) -> Result<u32> {
Ok(unsafe { self.0.GetCount()? })
}
fn get_at(&self, index: u32) -> Result<PROPERTYKEY> {
let mut key = PROPERTYKEY::default();
unsafe { self.0.GetAt(index, &mut key)? };
Ok(key)
}
fn get_value(&self, key: &PROPERTYKEY) -> Result<PROPVARIANT> {
Ok(unsafe { self.0.GetValue(key)? })
}
fn key_values(&self) -> impl Iterator<Item = (PROPERTYKEY, PROPVARIANT)> {
PropertyStoreIterator {
store: self,
current: 0,
}
}
}
struct PropertyStoreIterator<'a> {
store: &'a PropertyStore<'a>,
current: u32,
}
impl<'a> Iterator for PropertyStoreIterator<'a> {
type Item = (PROPERTYKEY, PROPVARIANT);
fn next(&mut self) -> Option<Self::Item> {
if self.current < self.store.get_count().ok()? {
let key = self.store.get_at(self.current).ok()?;
let value = self.store.get_value(&key).ok()?;
self.current += 1;
return Some((key, value));
}
None
}
}
fn get_full_path(path: &str) -> Result<HSTRING> {
let mut buf = vec![0; 260];
let mut file_part = PWSTR::null();
let len =
unsafe { GetFullPathNameW(&HSTRING::from(path), Some(&mut buf), Some(&mut file_part)) }
as usize;
ensure!(len != 0, "failed to get full path");
let buf = if buf.len() < len {
let mut buf = vec![0; len + 1];
let len =
unsafe { GetFullPathNameW(&HSTRING::from(path), Some(&mut buf), Some(&mut file_part)) };
ensure!(len != 0, "failed to get full path");
buf
} else {
buf
};
Ok(HSTRING::from_wide(&buf))
}
fn main() -> Result<()> {
let arg = env::args().nth(1).context("no args")?;
if arg.eq("-V") || arg.eq("--version") {
println!("property v{}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
let path = get_full_path(&arg)?;
let _com = Com::new()?;
let store: IPropertyStore =
unsafe { SHGetPropertyStoreFromParsingName(&path, None, GPS_OPENSLOWITEM)? };
let prop_system: IPropertySystem =
unsafe { CoCreateInstance(&CLSID_PropertySystem, None, CLSCTX_INPROC_SERVER)? };
println!("[");
PropertyStore::new(&store)
.key_values()
.filter_map(|(k, v)| {
let prop_desc: IPropertyDescription =
unsafe { prop_system.GetPropertyDescription(&k).ok()? };
let key = unsafe { prop_desc.GetDisplayName().ok()?.to_string().ok()? };
let value = unsafe {
prop_system
.FormatForDisplayAlloc(&k, &v, PDFF_DEFAULT)
.ok()?
.to_string()
.ok()?
.replace(r"\", r"\\")
};
Some((key, value))
})
.for_each(|(k, v)| {
println!("\t{{key: \"{k}\", value: \"{v}\"}},");
});
println!("]");
Ok(())
}
Discussion