LoginSignup
2

More than 3 years have passed since last update.

posted at

updated at

F#からNTFSの代替データストリームを利用する方法

TL;DR;

.NET Core / .NET Framework じゃ NTFS の ADS 使えないから大人しく P/Invoke 使ってね!

代替データストリームって何?

@minr さんの「代替データストリーム(ADS)について色々調べてみた」にだいたい書いてあるので一読してください。

F# で代替データストリームを触る

.NET の FileFileInfo, Directory, DirectoryInfo などからでは代替データストリームを作ったり、読んだりすることができないので、kernel32 の機能を利用しなければなりません。

P/Invoke にあまり慣れていない or 毎回C++のシグネチャからF#/C#のシグネチャに変換するのが面倒!っていう人は「PINVOKE.NET」という神サイトを利用しましょう。

🚨注意事項🚨

以下のサンプルでは文字エンコードを Shift_JIS として指定しています。
.NET Core で実行する場合、Nuget から System.Text を導入する必要があるので注意してください。

作成

まず、代替データストリームを作りたいファイルを用意します。

今回は C:/work を作業ディレクトリとして説明をしていきます。
コマンドコンソールについては PowerShell を利用しています。

cd C:/work
New-Item sample.txt

これで sample.txt という空のテキストファイルができました。
image.png

このファイルに通常のストリームだけしかないことを確認してみましょう。

Get-Item .\sample.txt -stream *

そうすると $Data というメインのストリームだけしかないことがわかります。
このファイルに 代替データストリーム を追加していきます。
image.png

次にネイティブメソッドを使うための定義をしていきます。
VSCode をインストールしている人であれば以下のコマンドを実行することで、VSCode が起動してくると思います。

New-Item NativeMethod.fsx
code .

それではこの NativeMethod.fsx ファイルにコードを追加していきます。
まず、ストリームを開いたり閉じたりするために CreateFileWCloseHandle という 2つ の関数を利用できるようにします。

module NativeMethod =

    open System
    open System.IO
    open System.Runtime.ConstrainedExecution
    open System.Runtime.InteropServices
    open System.Security
    open System.Text

    [<DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true, EntryPoint="CreateFileW");CompiledName("CreateFileW")>]
    extern nativeint createFile ( 
        [<MarshalAs(UnmanagedType.LPWStr); In>] string filename,
        [<MarshalAs(UnmanagedType.U4)>] FileAccess access,
        [<MarshalAs(UnmanagedType.U4)>] FileShare share,
        nativeint securityAttributes,
        [<MarshalAs(UnmanagedType.U4)>] FileMode creationDsiposition,
        [<MarshalAs(UnmanagedType.U4)>] FileAttributes flagsAndAttributes,
        nativeint templateFile )

    [<DllImport("kernel32.dll", SetLastError=true, EntryPoint="CloseHandle");CompiledName("CloseHandle")>]
    [<ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success);SuppressUnmanagedCodeSecurity>]
    extern [<MarshalAs(UnmanagedType.Bool)>] bool closeHandle(nativeint handle)

この関数を利用することで、ストリームの開閉ができるようになります。
open したら close することを忘れないようにしましょう。

では実際にストリームの開閉をしてみましょう。
処理のエントリポイントとなるファイルを作成し、そこに追記していきます。

New-Item Main.fsx
#load "NativeMethod.fsx"

open System
open System.IO
open NativeMethod

let nullptr = IntPtr.Zero
let handle = NativeMethod.createFile("sample.txt", FileAccess.ReadWrite, FileShare.Read, nullptr, FileMode.OpenOrCreate, FileAttributes.Normal, nullptr)
if handle <> nullptr then
    printfn "Success"
    NativeMethod.closeHandle(handle) |> ignore
else
    printfn "Failed"

これで Success が出力されれば成功です。
ここまでくれば、sample.txt に 代替データストリーム を作成することが可能です。

ファイル名を sample.txt から sample.txt:ストリーム名 に変更します。
今回は foo というストリーム名とし、 sample.txt:foo と指定しなおしました。

#load "NativeMethod.fsx"

open System
open System.IO
open NativeMethod

let nullptr = IntPtr.Zero

// ↓↓↓ この行を変更 ↓↓↓
let handle = NativeMethod.createFile("sample.txt:foo", FileAccess.ReadWrite, FileShare.Read, 
// ↑↑↑ この行を変更 ↑↑↑

nullptr, FileMode.OpenOrCreate, FileAttributes.Normal, nullptr)
if handle <> nullptr then
    printfn "Success"
    NativeMethod.closeHandle(handle) |> ignore
else
    printfn "Failed"

Success と出力されれば成功です。
以下のコマンドを実行すると、foo という代替データストリームが追加されたこと確認することができます。
Length の箇所がファイルサイズを示していますが、0byte であることがわかると思います。

Get-Item .\sample.txt -stream *

image.png

また、エクスプローラやコマンドプロンプトから一覧を見てみても、sample.txt:foo がパッと見では存在しているのかわかりません。
image.png

このように、ネイティブ関数を利用することで F# から代替データストリームを作成することができました。

書き込み

次に、代替データストリームにデータを書き込んでいきます。
NativeMethod.fsx に以下のコードを追加します。

[<DllImport("kernel32.dll", BestFitMapping=true, CharSet=CharSet.Ansi);CompiledName("WriteFile")>]
extern [<MarshalAs(UnmanagedType.Bool)>] bool writeFile(
    nativeint filehandle, 
    byte[] buffer,
    uint32 numberOfBytesToWrite,
    uint32& numberOfBytesWritten,
    nativeint overlapped )

このままでは使いにくいので適当な関数を用意します。
今回は Main.fsx にコードを追加しました。

// 本来であればもう少し処理を細分化すべきだが、今回はサンプルなので良しとする
let write (str:string) handle =
    let bytes = (Encoding.GetEncoding("Shift_JIS")).GetBytes(str.ToCharArray())
    let length = Array.length >> uint32
    let mutable size = 0u
    NativeMethod.writeFile(handle, bytes, length bytes, &size, nullptr)

これで代替データストリームに書き込むための準備ができました。
今回作った write関数 を利用して、実際に代替データストリームへ書き込みを行ってみます。
if文の中身を以下のように修正します。

if handle <> nullptr then
    printfn "Success"
    // ↓↓↓ この行を追加 ↓↓↓
    write "ほげほげ" handle |> ignore
    // ↑↑↑ この行を追加 ↑↑↑
    NativeMethod.closeHandle(handle) |> ignore
else
    printfn "Failed"

ここまでで Main.fsx は以下のようになっているはずです。

#load "NativeMethod.fsx"

open System
open System.IO
open System.Text
open NativeMethod

let nullptr = IntPtr.Zero

let write (str:string) handle =
    let bytes = (Encoding.GetEncoding("Shift_JIS")).GetBytes(str.ToCharArray())
    let length = Array.length >> uint32
    let mutable size = 0u
    NativeMethod.writeFile(handle, bytes, length bytes, &size, nullptr)

let handle = NativeMethod.createFile("sample.txt:foo", FileAccess.ReadWrite, FileShare.Read, nullptr, FileMode.OpenOrCreate, FileAttributes.Normal, nullptr)
if handle <> nullptr then
    printfn "Success"
    write "ほげほげ" handle |> ignore
    NativeMethod.closeHandle(handle) |> ignore
else
    printfn "Failed"

それでは早速これを実行してみましょう。
おそらく Success が出力されると思います。

では、Get-Item コマンドでもう一度 sample.txt のストリーム情報を見てみましょう。

Get-Item .\sample.txt -stream *

すると Length8 と変化していることがわかると思います。
また、本体の $Data0 のままであることがわかります。

image.png

この代替データストリームのサイズは、通常エクスプローラ等の GUI からでは伺いしることはできません。
もちろん中身を知ることもできません。

では、CUI から中身を確認してみましょう。

# "Get-Content ファイル名 -stream 代替データストリーム名" で値を得られる
Get-Content .\sample.txt -stream foo       

image.png
どうやらしっかりと書き込めているようですね!
もちろん sample.txt の本体には何も書き込まれていません。
image.png

読み込み

では F# 上から代替データストリーム上のデータを読み取ってみましょう。
NativeMethod.fsx に以下の 2つ の関数を追加します。

[<DllImport("kernel32.dll", SetLastError=true);CompiledName("ReadFile")>]
extern bool readFile(
    nativeint filehandle, 
    [<Out>] byte[] buffer,
    uint32 numberOfBytesToRead, 
    uint32& numberOfBytesRead, 
    nativeint overlapped )

[<DllImport("kernel32.dll", EntryPoint="GetFileSizeEx");CompiledName("GetFileSize")>]
extern [<MarshalAs(UnmanagedType.Bool)>] bool getFileSize(
    nativeint filehandle,
    [<Out>] int64& filesize);    

例のごとく、このままでは使いにくいので適当な関数を作成します。
今回も NativeMethod.fsx に以下のようなコードを追加しました。

let read handle =
    let mutable size = 0
    NativeMethod.getFileSize(handle, &size) |> ignore
    let mutable buf = Array.zeroCreate (int size)
    let mutable readsize = 0u
    NativeMethod.readFile(handle, buf, uint32 size, &readsize, nullptr) |> ignore
    (Encoding.GetEncoding("Shift_JIS")).GetString(buf)

この関数を使って、実際に代替データストリームから値を読み取ってみましょう。
Main.fsx を以下のように書き換えました。

#load "NativeMethod.fsx"

open System
open System.IO
open System.Text
open NativeMethod

let nullptr = IntPtr.Zero

let write (str:string) handle =
    let bytes = (Encoding.GetEncoding("Shift_JIS")).GetBytes(str.ToCharArray())
    let length = Array.length >> uint32
    let mutable size = 0u
    NativeMethod.writeFile(handle, bytes, length bytes, &size, nullptr)

let read handle =
    let mutable size = 0
    NativeMethod.getFileSize(handle, &size) |> ignore
    let mutable buf = Array.zeroCreate (int size)
    let mutable readsize = 0u
    NativeMethod.readFile(handle, buf, uint32 size, &readsize, nullptr) |> ignore
    (Encoding.GetEncoding("Shift_JIS")).GetString(buf)

// ↓↓↓ ここを修正 ↓↓↓
let h = NativeMethod.createFile("sample.txt:foo", FileAccess.ReadWrite, FileShare.Read, nullptr, FileMode.OpenOrCreate, FileAttributes.Normal, nullptr)
if h <> nullptr then
    printfn "=== Write ==="
    write "ほげほげ" h |> ignore
    NativeMethod.closeHandle(h) |> ignore
else
    printfn "Failed"
// ↑↑↑ ここを修正 ↑↑↑

// ↓↓↓ ここを追加 ↓↓↓    
let h' = NativeMethod.createFile("sample.txt:foo", FileAccess.ReadWrite, FileShare.Read, nullptr, FileMode.OpenOrCreate, FileAttributes.Normal, nullptr)
if h' <> nullptr then
    printfn "=== Read ==="
    read h' |> printfn "read value= %s"
    NativeMethod.closeHandle(h') |> ignore
else
    printfn "Failed"
// ↑↑↑ ここを追加 ↑↑↑

これを実行すると以下のように出力されます。
image.png

これで F# から代替データストリームの値を読み取ることができるようになりました。

おわりに

.NET系の言語で代替データストリームを扱うのは P/Invoke からでないと現状無理なので中々面倒です。
しかし、一度扱い方さえわかってしまえば楽ですね。

また、F#P/Invoke を使う際は呼び出し層を低レイヤーにして隠蔽する努力も必要だったりするので、実際のコーディングでは設計に気をつけましょう。

当然のことながら今回の記事のサンプルコードは多くの問題を孕んでいるので、そのままの状態でプロダクトコードとして利用しようとは思ってはなりません。
関数を適切に分割したり、ネイティブ関数からの戻り値を見て適宜分岐を挟んだり、型をきちんと作成したり、値のチェックをしたりと、いろいろしなければならないことを省略しまっくています。


今回のコードサンプルは GitHub Gist で公開しているのでご参照ください。

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
What you can do with signing up
2