0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PowerShell で LZMS なる圧縮アルゴリズムを試さむとしてするなり

Last updated at Posted at 2025-10-04

TL;DR

本記事の内容は実用性重点というより検証過程のメモに近いものです。追加インストールなしにコマンドラインで高圧縮したいケースなら、Windows 11 付属の tar.exe で 7z を指定する方法をおすすめします。

LZMS ってなんだ?

Windows 11 Build 25992 以降、エクスプローラーが 7z や tar などの圧縮・解凍を標準でサポートするようになりました。この機能は libarchivearchiveint.dll)により実現されていますが、bsdtar が tar.exe として同梱されているのでコマンドラインでも圧縮・解凍できます

一方で、Compress-Archive / Expand-Archive コマンドレットでは依然として ZIP 形式しかサポートされていないようです。ですが、この探索の過程で Windows 10 Build 10240 から WinRT API を通じて LZMS なる圧縮アルゴリズムが利用できるという情報をキャッチしました。LZMA の誤字ではなく、です。

LZMA と LZMS の違い

LZMA (Lempel-Ziv-Markov chain Algorithm) は、7-Zip の作者である Igor Pavlov 氏によって開発された、非常に圧縮率の高いアルゴリズムです。現在ではその改良版である LZMA2 が.7z 形式のデフォルトアルゴリズムとして採用されています。7-zip の大部分には GNU LGPL が適用されますが、LZMA SDK はパブリックドメインで公開されています。

対する LZMS ですが、こちらは何の略称なのか公式な情報源を見つけることができませんでした。というのも、LZMS は Microsoft が Windows のインストールイメージ (.esd ファイルなど) で利用するために開発した独自アルゴリズムのようなのです。2012 年ごろにリリースされたらしく、Windows 8 または Windows Server 2012 以降で使用できる Win32 Compression API のドキュメントにも登場します。その仕様は公開されていませんが、リバースエンジニアリングによって開発された wimlib というオープンソース実装が存在します。作者の ebiggers 氏曰く、当該リポジトリに含まれる lzms_decompress.c 内のコメントとコードが、LZMS に関する入手可能な最良のドキュメントである可能性があるとのことです。1

wimlib の解説によると、LZMS は LZ77 系の辞書式符号化をベースとしている点で LZMA と同様です。エントロピー符号化の観点では LZMA がレンジ符号を使用しているのに対し、LZMS はレンジ符号とハフマン符号のハイブリッドになっています。ハフマン符号はレンジ符号に比べて仕組みがシンプルで、一般的に展開処理が高速です。おそらく、LZMS はインストールイメージという高圧縮率と展開速度のバランスが求められる用途に最適化されたアルゴリズムなのでしょう。

PowerShell で LZMS 圧縮・解凍をやってみる

さて、PowerShell からは直接利用できない LZMS ですが、WinRT API を少し工夫して呼び出すことで、PowerShell スクリプトからでも利用できます。なお、本コードは microsoft/Windows-universal-samples の Samples/Compression を PowerShell に移植したものです。

Compress-Lzms.ps1
try {
    # WinRT アセンブリから必要なクラスをロード
    Add-Type -AssemblyName System.Runtime.WindowsRuntime
    [void][Windows.Storage.StorageFile, Windows.Storage, ContentType = WindowsRuntime]
    [void][Windows.Storage.Streams.RandomAccessStream, Windows.Storage.Streams, ContentType = WindowsRuntime]
    [void][Windows.Storage.Compression.Compressor, Windows.Storage.Compression, ContentType = WindowsRuntime]
    [void][Windows.Storage.Compression.Decompressor, Windows.Storage.Compression, ContentType = WindowsRuntime]

    # GetAwaiter<TResult>メソッドを取得
    $script:getAwaiter = [WindowsRuntimeSystemExtensions].GetMember('GetAwaiter', 'Method', 'Public,Static') |
    Where-Object { $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperation`1' } |
    Select-Object -First 1

    # GetAwaiter<TResult,TProgress>メソッドを取得
    $script:getAwaiterWithProgress = [WindowsRuntimeSystemExtensions].GetMember('GetAwaiter', 'Method', 'Public,Static') |
    Where-Object { $_.GetParameters()[0].ParameterType.Name -eq 'IAsyncOperationWithProgress`2' } |
    Select-Object -First 1
}
catch {
    Write-Error "初期化中にエラーが発生しました: $($_.Exception.Message)"
}

# 非同期操作を同期的に実行するヘルパー関数
function Invoke-Async {
    param (
        [object]$AsyncTask, # 非同期操作(IAsyncOperation もしくは IAsyncOperationWithProgress)
        [Type]$ResultType,  # 戻り値の型
        [Type]$ProgressType # 進捗の型 (IAsyncOperation の場合は指定しない)
    )

    try {
        if ($ProgressType -eq $null) {
            # 進捗なしの非同期操作
            $genericMethod = $getAwaiter.MakeGenericMethod($ResultType)
        }
        else {
            # 進捗付きの非同期操作
            $genericMethod = $getAwaiterWithProgress.MakeGenericMethod($ResultType, $ProgressType)
        }

        $awaiter = $genericMethod.Invoke($null, @($AsyncTask))
        return $awaiter.GetResult()
    }
    catch {
        Write-Error "非同期操作の実行中にエラーが発生しました: $($_.Exception.Message)"
    }
}

# ソースファイルのストリームを開く内部用関数
function Get-SourceStream {
    param (
        [string]$Path # ソースのパス
    )

    try {
        # ソースファイルの取得
        $getSourcefileTask = [Windows.Storage.StorageFile]::GetFileFromPathAsync($Path)
        $sourceFile = Invoke-Async $getSourcefileTask -ResultType ([Windows.Storage.StorageFile])

        # ソースファイルのストリームを開く
        $openSourceFileTask = $sourceFile.OpenAsync([Windows.Storage.FileAccessMode]::Read)
        return Invoke-Async $openSourceFileTask -ResultType ([Windows.Storage.Streams.IRandomAccessStream])
    }
    catch {
        Write-Error "ファイル '$Path' の読込中にエラーが発生しました。詳細: $($_.Exception.Message)"
    }
}

# 出力先のパスに空ファイルを作成してストリームを開く内部用関数
function Get-DestStream {
    param (
        [string]$Output # ソースのパス
    )

    try {
        # 出力先のパスに空ファイルを作成
        [void](New-Item $Output -ItemType file -Force)

        # 出力先ファイルの取得
        $getDestfileTask = [Windows.Storage.StorageFile]::GetFileFromPathAsync($Output)
        $destFile = Invoke-Async $getDestfileTask -ResultType ([Windows.Storage.StorageFile])

        # 出力先ファイルのストリームを開く
        $openDestFileTask = $destFile.OpenAsync([Windows.Storage.FileAccessMode]::ReadWrite)
        return Invoke-Async $openDestFileTask -ResultType ([Windows.Storage.Streams.IRandomAccessStream])
    }
    catch {
        Write-Error "ファイル '$Output' の読込中にエラーが発生しました。詳細: $($_.Exception.Message)"
    }
}

# LZMS 形式で圧縮するコマンドレット
function Compress-Lzms {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$Path, # ソースのパス

        [Parameter(Mandatory)]
        [string]$Output # 出力先のパス
    )

    $sourceFileStream = $null
    $destFileStream = $null
    $compressor = $null

    try {
        # ソースファイルのストリームを開く
        $sourceFileStream = Get-SourceStream -Path $Path

        # 出力先ファイルのストリームを開く
        $destFileStream = Get-DestStream -Output $Output

        # LZMS 圧縮アルゴリズムを使用する Compressor インスタンスを作成
        # ブロックサイズ: 0 = デフォルト
        $compressor = New-Object Windows.Storage.Compression.Compressor ($destFileStream.GetOutputStreamAt(0), [Windows.Storage.Compression.CompressAlgorithm]::Lzms, 0)

        # ソースファイルのストリームを Compressor インスタンスにコピー
        $copyTask = [Windows.Storage.Streams.RandomAccessStream]::CopyAsync($sourceFileStream, $compressor)
        $bytesProcessed = Invoke-Async $copyTask -ResultType ([UInt64]) -ProgressType ([UInt64])

        # 終了処理
        [void](Invoke-Async $compressor.FinishAsync() -ResultType ([Boolean]))

        Write-Verbose "処理したバイト数: $bytesProcessed"
        Write-Verbose "圧縮後のバイト数: $($destFileStream.size)"
    }
    catch {
        Write-Error "ファイル '$Path' の圧縮中にエラーが発生しました。詳細: $($_.Exception.Message)"
    }
    finally {
        # リソースを解放
        if ($null -ne $compressor) { $compressor.Dispose() }
        if ($null -ne $destFileStream) { $destFileStream.Dispose() }
        if ($null -ne $sourceFileStream) { $sourceFileStream.Dispose() }
        Write-Verbose "すべてのリソースを解放しました。"
    }
}

# LZMS 形式のデータを解凍するコマンドレット
function Expand-Lzms {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$Path, # ソースのパス
        
        [Parameter(Mandatory)]
        [string]$Output # 出力先のパス
    )
    
    $sourceFileStream = $null
    $destFileStream = $null
    $decompressor = $null

    try {
        # ソースファイルのストリームを開く
        $sourceFileStream = Get-SourceStream -Path $Path

        # 出力先ファイルのストリームを開く
        $destFileStream = Get-DestStream -Output $Output

        # Decompressor インスタンスを作成
        $decompressor = New-Object Windows.Storage.Compression.Decompressor ($sourceFileStream)

        # Decompressor インスタンスのストリームを出力先にコピー
        $copyTask = [Windows.Storage.Streams.RandomAccessStream]::CopyAsync($decompressor, $destFileStream)
        $bytesProcessed = Invoke-Async $copyTask -ResultType ([UInt64]) -ProgressType ([UInt64])
    
        Write-Verbose "処理したバイト数: $bytesProcessed"
    }
    catch {
        Write-Error "ファイル '$Path' の解凍中にエラーが発生しました。詳細: $($_.Exception.Message)"
    }
    finally {
        # リソースを解放
        if ($null -ne $decompressor) { $decompressor.Dispose() }
        if ($null -ne $destFileStream) { $destFileStream.Dispose() }
        if ($null -ne $sourceFileStream) { $sourceFileStream.Dispose() }
        Write-Verbose "すべてのリソースを解放しました。"
    }
}

使い方

このスクリプトには Compress-LzmsExpand-Lzms という 2 つの関数が含まれています。

# コマンドレットをインポートする
. .\Compress-Lzms.ps1

# 圧縮
Compress-Lzms -Path "元のファイル" -Output "圧縮後.lzmsdat"

# 解凍
Expand-Lzms -Path "圧縮後.lzmsdat" -Output "解凍後のファイル"

念のため、出力されたデータは生の LZMS データであって、7-Zip 等で開くことができる WIM 形式アーカイブではありません。

なお、動作環境は Windows 10 以降の WinRT API が利用可能な環境で実行された Windows PowerShell(powershell.exe)です。PowerShell Core(pwsh.exe)では以下エラーとなり実行できません:

Write-Error: 初期化中にエラーが発生しました: Unable to find type [Windows.Storage.StorageFile,Windows.Storage, ContentType=WindowsRuntime].

内部の解説

WinRT API のメソッドは基本的に非同期 (IAsyncOperation<T>IAsyncOperationWithProgress<T,U>) となっています。そのままでは PowerShell から扱いづらいため、同期的に結果を取得するヘルパー関数 Invoke-Async を用意しています。これは TobiasPSP/PsOcraburaage3 氏の投稿が紹介しているものに基づきますが、IAsyncOperationWithProgress<T,U> にも対応させたのが特色です。

まとめ

当初の目的だった「PowerShell で LZMA 圧縮・解凍してみる」ことは叶いませんでしたが、その過程で LZMS という面白い技術を発見できました。実用面では tar.exe で 7z を指定する方法を推奨しますが、WinRT API を PowerShell から呼び出すテクニックとしても参考になれば幸いです。

余談、あるいは「生成 AI 開発の珍プレー」キャンペーンに寄せたこぼれ話

初期の調査において Copilot や Gemini に対し(Web グラウンディングを有効にした状態で)LZMS に関する質問をしたものの、マイナーすぎるためか適切な回答をしてくれませんでした:

結果としては Gemini の DeepResearch が wimlib の存在を教えてくれたので、そこを踏み台として理解を深めていきました。専門外の分野だと同様レベルのミスをしていても気が付けなさそう(特に Gemini のポッドキャスト風レポートなどの形式では)なので、普段から信用しすぎないように気を付けたいものです。

参考リンク

  1. https://github.com/ebiggers/wimlib/blob/master/README.md#references

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?