TL;DR;
.NET Core / .NET Framework じゃ NTFS の ADS 使えないから大人しく P/Invoke 使ってね!
代替データストリームって何?
@minr さんの「代替データストリーム(ADS)について色々調べてみた」にだいたい書いてあるので一読してください。
F# で代替データストリームを触る
.NET の File
や FileInfo
, 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
という空のテキストファイルができました。
このファイルに通常のストリームだけしかないことを確認してみましょう。
Get-Item .\sample.txt -stream *
そうすると $Data
というメインのストリームだけしかないことがわかります。
このファイルに 代替データストリーム
を追加していきます。
次にネイティブメソッドを使うための定義をしていきます。
VSCode をインストールしている人であれば以下のコマンドを実行することで、VSCode が起動してくると思います。
New-Item NativeMethod.fsx
code .
それではこの NativeMethod.fsx
ファイルにコードを追加していきます。
まず、ストリームを開いたり閉じたりするために CreateFileW
と CloseHandle
という 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 *
また、エクスプローラやコマンドプロンプトから一覧を見てみても、sample.txt:foo
がパッと見では存在しているのかわかりません。
このように、ネイティブ関数を利用することで 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 *
すると Length
が 8
と変化していることがわかると思います。
また、本体の $Data
は 0
のままであることがわかります。
この代替データストリームのサイズは、通常エクスプローラ等の GUI からでは伺いしることはできません。
もちろん中身を知ることもできません。
では、CUI から中身を確認してみましょう。
# "Get-Content ファイル名 -stream 代替データストリーム名" で値を得られる
Get-Content .\sample.txt -stream foo
どうやらしっかりと書き込めているようですね!
もちろん sample.txt
の本体には何も書き込まれていません。
読み込み
では 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"
// ↑↑↑ ここを追加 ↑↑↑
これで F#
から代替データストリームの値を読み取ることができるようになりました。
おわりに
.NET系の言語で代替データストリームを扱うのは P/Invoke
からでないと現状無理なので中々面倒です。
しかし、一度扱い方さえわかってしまえば楽ですね。
また、F#
で P/Invoke
を使う際は呼び出し層を低レイヤーにして隠蔽する努力も必要だったりするので、実際のコーディングでは設計に気をつけましょう。
当然のことながら今回の記事のサンプルコードは多くの問題を孕んでいるので、そのままの状態でプロダクトコードとして利用しようとは思ってはなりません。
関数を適切に分割したり、ネイティブ関数からの戻り値を見て適宜分岐を挟んだり、型をきちんと作成したり、値のチェックをしたりと、いろいろしなければならないことを省略しまっくています。
今回のコードサンプルは GitHub Gist で公開しているのでご参照ください。