LoginSignup
4
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-06

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 で公開しているのでご参照ください。

4
2
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
4
2