🕐

Windowsのファイル時刻を扱うためのライブラリを作成した話

2024/03/02に公開

RustでWindowsのファイル時刻を扱うためのライブラリのnt-timeを開発したので紹介します。

https://github.com/sorairolake/nt-time

ファイル時刻とは

nt-timeクレートについて説明する前に、ファイル時刻について説明します。

https://learn.microsoft.com/ja-jp/windows/win32/sysinfo/file-times

ファイル時刻は、1601年1月1日0時0分0秒 (UTC) から経過した100ナノ秒間隔の数を表す64ビット整数値で、Unix系のシステムにおけるUNIX時間に相当するものです。
ファイル時刻は、NTFSや7zのタイムスタンプとして利用されています[1]
ファイル時刻は、Win32 APIではGetSystemTime()を使用することで取得することができ、.NETではDateTime.ToFileTime()を使用することで取得することができます。
最大値は60056年5月28日5時36分10秒955161500 (UTC) ですが、WindowsのAPIでは最大値は64ビット符号付き整数の最大値に制限されます。

nt-timeとは

nt-timeクレートはこのファイル時刻を扱うためのライブラリです。

ファイル時刻を表す型としてnt_time::FileTimeを定義しています。
この型はNew Type Patternを利用しており、内部的にはu64です。

FileTimeは以下のような機能を実装しています。

  1. core::time::Durationなどとの加算や減算
  2. stdtimeクレートやchronoクレートの時間を表す型との相互変換
  3. UNIX時間やMS-DOSの日付と時刻との相互変換
  4. Serdeを利用したシリアライズとデシリアライズ

コード例

use core::time::Duration;

use nt_time::{
    time::{macros::datetime, OffsetDateTime},
    FileTime,
};

let ft = FileTime::NT_TIME_EPOCH;
assert_eq!(
    OffsetDateTime::try_from(ft).unwrap(),
    datetime!(1601-01-01 00:00 UTC)
);

let ft = ft + Duration::from_secs(11_644_473_600);
assert_eq!(
    OffsetDateTime::try_from(ft).unwrap(),
    OffsetDateTime::UNIX_EPOCH
);
assert_eq!(ft.to_raw(), 116_444_736_000_000_000);

assert_eq!(FileTime::new(u64::MAX), FileTime::MAX);

定数

以下の3つの定数を定義しています。

演算

FileTimeAddを実装しているので、core::time::Durationtime::Durationの値を加算することができます。
Subではこれらに加えてstd::time::SystemTimeなどの時間を表す型との演算を実装しているので、FileTimeからこれらの型の値を減算することができます。
core::time::Durationとの演算ではオーバーフローした場合に飽和するメソッド (FileTime::saturating_add()FileTime::saturating_sub()) やNoneを返すメソッド (FileTime::checked_add()FileTime::checked_sub()) を定義しています。
また、PartialEqPartialOrdも実装しているので、これらの期間や時間を表す型との同値関係と順序関係を比較することができます。

相互変換

FileTimeFromTryFromによってstd::time::SystemTimetime::OffsetDateTimechrono::DateTime<chrono::offset::Utc>と相互変換することができます。
これらは変換元の値が変換先の型の範囲外の場合はTryFromを使うようになっています。
また、u64の値からFileTimeを作成すること (FileTime::new()) とその逆 (FileTime::to_raw()) にも対応しています。

UNIX時間との相互変換については、秒単位とナノ秒単位に対応しています。
MS-DOSの日付と時刻との相互変換については、exFATなどの10ミリ秒単位の分解能とUTCオフセットに対応しています。

use core::time::Duration;

use nt_time::{
    time::{OffsetDateTime, UtcOffset},
    FileTime,
};

// `1970-01-01 00:00:00 UTC`.
let ut = i64::default();
assert_eq!(
    OffsetDateTime::from_unix_timestamp(ut).unwrap(),
    OffsetDateTime::UNIX_EPOCH
);

let ft = FileTime::from_unix_time(ut).unwrap();
assert_eq!(ft, FileTime::UNIX_EPOCH);

// `1980-01-01 00:00:00 UTC`.
let ft = ft + Duration::from_secs(315_532_800);
let dos_dt = ft.to_dos_date_time(Some(UtcOffset::UTC)).unwrap();
assert_eq!(dos_dt, (0x0021, u16::MIN, u8::MIN, Some(UtcOffset::UTC)));

シリアライズとデシリアライズ

Serdeを利用してISO 8601形式、RFC 2822形式、RFC 3339形式、UNIX時間との間でシリアライズとデシリアライズを行うことができます。

use nt_time::{
    serde::{Deserialize, Serialize},
    serde_with::iso_8601,
    FileTime,
};

#[derive(Debug, Deserialize, PartialEq, Serialize)]
struct DateTime(#[serde(with = "iso_8601")] FileTime);

let json = serde_json::to_string(&DateTime(FileTime::UNIX_EPOCH)).unwrap();
assert_eq!(json, r#""+001970-01-01T00:00:00.000000000Z""#);

assert_eq!(
    serde_json::from_str::<DateTime>(&json).unwrap(),
    DateTime(FileTime::UNIX_EPOCH)
);

終わりに

RustでWindowsのファイル時刻を扱うためのライブラリのnt-timeクレートを紹介しました。

詳細なドキュメントはDocs.rsで確認することができます。

脚注
  1. https://py7zr.readthedocs.io/en/v0.20.8/archive_format.html#filetime ↩︎

GitHubで編集を提案

Discussion