パスの形式を正規化する

Recipe ID: fs-022

パスに含まれる冗長なセグメント(.., .)や、重複する区切り文字を解決し、標準的な形式(正規化されたパス)に変換する方法を解説します。
ユーザー入力や相対パスの計算によって複雑になったパスをクリーンにするために使用します。

使用方法

@tauri-apps/api/path から normalize をインポートして使用します。

import { normalize } from '@tauri-apps/api/path';

// 冗長なパスを解決
const cleanPath = await normalize('/foo/bar//baz/../quux');
// 結果: /foo/bar/quux

1. フロントエンドから作成する (TypeScript)

@tauri-apps/api/pathnormalize 関数を使用します。

複雑な相対パスを解決する

..(親ディレクトリ)や .(カレントディレクトリ)が含まれるパスを解決してシンプルにします。

import { normalize } from '@tauri-apps/api/path';

async function performNormalization() {
    try {
        const messyPath = 'documents/projects/../notes/./memo.txt';
        const cleanPath = await normalize(messyPath);
        
        console.log(`Original: ${messyPath}`);
        console.log(`Normalized: ${cleanPath}`);
        // Output: documents/notes/memo.txt (OSによって区切り文字は異なる)
        
        return cleanPath;
    } catch (err) {
        console.error('Normalization failed:', err);
    }
}

ユーザー入力パスのサニタイズ

ユーザーが入力したパス文字列をシステムで使用する前に正規化する場合などに役立ちます。

import { normalize, join } from '@tauri-apps/api/path';

async function sanitizeUserInput(baseDir: string, userInput: string) {
    // ユーザーが "../../../etc/passwd" のような入力をしても
    // join と normalize を通すことで意図したベースディレクトリからの相対関係を解決できます
    // ただし、ディレクトリトラバーサル攻撃を防ぐには、
    // 正規化後のパスが baseDir で始まっているかどうかのチェックも必要です。
    
    // 注意: join自体も内部でnormalizeを行うことが多いですが、明示的に行う例です
    const rawPath = await join(baseDir, userInput);
    const cleanPath = await normalize(rawPath);
    
    return cleanPath;
}

2. バックエンドから作成する (Rust)

Rust では std::fs::canonicalize を使用することで、パスの正規化を行えます(シンボリックリンクの解決も行います)。
ただし、canonicalizeファイルの存在を確認するため、存在しないパスに対してはエラーを返します。

存在するパスの正規化(絶対パスへ)

use std::fs;

#[tauri::command]
fn normalize_existing_path(path: String) -> Result<String, String> {
    // 存在しない場合はエラーになります
    let pb = fs::canonicalize(&path).map_err(|e| e.to_string())?;
    Ok(pb.to_string_lossy().into_owned())
}

文字列としての正規化(存在チェックなし)

標準ライブラリには純粋な文字列操作としての normalize はありませんが、Path::components を使って自前で実装するか、path-clean などのクレートを使用するのが一般的です。ここでは簡易的な実装例を示します。

use std::path::{Path, PathBuf, Component};

#[tauri::command]
fn normalize_path_string(path: String) -> String {
    let mut normalized = PathBuf::new();
    for component in Path::new(&path).components() {
        match component {
            Component::ParentDir => { normalized.pop(); },
            Component::CurDir => {}, // 無視
            c => normalized.push(c),
        }
    }
    normalized.to_string_lossy().into_owned()
}

補足

  • normalize: 文字列としての正規化を行うため、実際のファイルシステムの状況(シンボリックリンクかどうかなど)は考慮しません。純粋なパス文字列の整理に使用します。
  • セキュリティ: ユーザー入力を扱う場合、アップロードされたファイル名や指定されたパスに .. が含まれていると、意図しないディレクトリにアクセスされる(ディレクトリトラバーサル)危険性があります。normalize した後、許可されたルートディレクトリ内にあるかを必ず確認してください。