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

# TL;DR;

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


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

@minr さんの「[代替データストリーム（ADS）について色々調べてみた](https://qiita.com/minr/items/c2393f532b2df35f7a9d)」にだいたい書いてあるので一読してください。

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

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

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

## 🚨注意事項🚨
以下のサンプルでは文字エンコードを `Shift_JIS` として指定しています。
`.NET Core` で実行する場合、`Nuget` から [**System.Text**](https://www.nuget.org/packages/System.Text.Encoding/) を導入する必要があるので注意してください。


## 作成

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

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

```powershell
cd C:/work
New-Item sample.txt
```

これで `sample.txt` という空のテキストファイルができました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125601/416653e8-5d29-2dfd-08c0-d185b03b067a.png)

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

```powershell
Get-Item .\sample.txt -stream *
```

そうすると `$Data` というメインのストリームだけしかないことがわかります。
このファイルに `代替データストリーム` を追加していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125601/f4c67026-f3e0-1f19-e786-9e12d7d1d6fb.png)



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

```powershell
New-Item NativeMethod.fsx
code .
```

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

```fsharp
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` することを忘れないようにしましょう。

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

```powershell
New-Item Main.fsx
```

```fsharp
#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` と指定しなおしました。

```fsharp
#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 であることがわかると思います。

```powershell
Get-Item .\sample.txt -stream *
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125601/54d623ed-305d-1928-31ff-a3c098dd4497.png)

また、エクスプローラやコマンドプロンプトから一覧を見てみても、`sample.txt:foo` がパッと見では存在しているのかわかりません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125601/34a845f4-1cdf-5f6f-a5fa-d2d88a52b34f.png)

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


## 書き込み

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

```fsharp
[<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` にコードを追加しました。

```fsharp
// 本来であればもう少し処理を細分化すべきだが、今回はサンプルなので良しとする
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文の中身を以下のように修正します。

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

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

```fsharp
#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` のストリーム情報を見てみましょう。

```powershell
Get-Item .\sample.txt -stream *
```

すると `Length` が `8` と変化していることがわかると思います。
また、本体の `$Data` は `0` のままであることがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125601/e68ae632-cf91-31de-0ae3-dfa1486cd949.png)

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

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

```powershell
# "Get-Content ファイル名 -stream 代替データストリーム名" で値を得られる
Get-Content .\sample.txt -stream foo       
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125601/5dca034d-b55e-4184-65da-3f20d8f47595.png)
どうやらしっかりと書き込めているようですね！
もちろん `sample.txt` の本体には何も書き込まれていません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125601/552d431b-ef0d-3c41-7b84-5888f0379ee3.png)


## 読み込み

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

```fsharp
[<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` に以下のようなコードを追加しました。

```fsharp
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` を以下のように書き換えました。

```fsharp
#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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125601/3d77c682-5107-aefd-5014-dad64c3657ed.png)

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


# おわりに

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

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

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

---

今回のコードサンプルは [**GitHub Gist**](https://gist.github.com/Midoliy/283b84c377ec9af225f81ba48c775b9a) で公開しているのでご参照ください。