はじめに
正しい字幕変換は、最初の正しい例より後にある字幕も含めて、ファイル全体を保護する必要があります。後のキューの形式が不正でも部分的な結果を残してはならず、新しい実行が成功したように見えても既存の出力を上書きしてはいけません。
完全なキュー変換を囲む Rust のファイル操作境界を実装し、Unicode、複数行テキスト、有効な重なり、無効なシフト、存在しないパス、出力の競合を試します。この独立環境には、前のラボの完成済みミリ秒変換関数とシフト関数が提供されています。その VM のファイルは必要ありません。準備済みプレイヤーは実際に選択した出力を読み、元ファイルから字幕を再構築しません。
前のラボの形式と時間規則、およびそこで示した Rust の前提知識を理解している必要があります。このラボで追加するのは新規出力作成の境界です。動画エンコードを教えたり、WebVTT の全構文に対応すると主張したりはしません。著者の見積もりは、読解と通常の復旧を含めて 50 分で、初心者の実測時間ではありません。
出力を作る前にすべてを検証する
このステップでは、読み取り、検証、新規ファイル作成の完全な順序を実装します。Terminal を開き、Cargo をこのシェルのパスに加え、セッションで利用できる UTF-8 ロケールを選択し、独立プロジェクトへ移動します。
export PATH=/home/labex/.cargo/bin:$PATH
export LANG=C.UTF-8 LC_ALL=C.UTF-8
cd /home/labex/project/subtitle-sync
新しいターミナルではエクスポートを繰り返し、場所が分からなくなったらディレクトリ移動コマンドを再実行します。起動ファイルは編集しません。ターミナルの Unicode 字形が代替記号になる場合でも、実際の UTF-8 ファイルバイトとブラウザー字幕はテキストを保持します。
編集するのは src/export.rs だけで、transform(input: &Path, output: &Path, delta: i64) -> Result<usize, String> を保持します。他のすべてのソースファイル、Cargo.toml、rust-toolchain.toml、および data/ の元ファイルは変更しません。src/time.rs には既に完成した時間関数があります。公開された前提は、パーサー検証済みの非負の時間要素と、有効な Cue オブジェクトのままです。同等の内部処理順序を実装しても構いませんが、出力作成前にすべての検証を完了する必要があります。変更しない準備済みファイルはバイト、権限、所有者/グループ、変更時刻を保持し、通常の読み取りによるアクセス時刻は対象外です。
前のラボの、制限されたプレーンテキスト WebVTT サブセットをここでも使います。正確な WEBVTT ヘッダーと、その次の空行が必要です。時間行は HH:MM:SS.mmm --> HH:MM:SS.mmm で、示された空白を正確に使い、数字は ASCII、時は 2 桁以上、分と秒は 00–59 の 2 桁、ミリ秒は 3 桁です。完全な時刻は非負の i64 ミリ秒に収まる必要があります。開始時刻は減少せず、同一開始時刻と重なりは有効で、終了は開始を上回ります。キューは空でない論理的な字幕行を 1 行以上持ちます。Unicode、タブ、複数行、先頭/末尾の空白を保ち、空白文字だけの本文も含みます。本当に空の行がキューを終え、その後の孤立したテキストは拒否されます。入力は LF/CRLF、末尾の改行の有無、追加の空の区切りを認めます。準備済み出力は LF を使い、0–99 時間では時を 2 桁で表し、それより大きい時は必要な桁だけ追加します。ヘッダー後と最後を含む各キュー後に空行を 1 行置きます。時刻の表記と区切り数は正規化しますが、論理的な字幕テキストを保ちます。単独のキャリッジリターン、BOM/ヘッダーメタデータ、キュー ID/設定、NOTE/STYLE/REGION、マークアップ/実体参照、本文の <、>、&、タブ以外の制御文字はこのコースでは未対応であり、黙って削除するのではなく拒否します。
同じ実際のソースを確認し、意図的に未完成のエクスポートを試します。
cargo run --quiet -- inspect data/original.vtt
cargo run --quiet -- shift data/original.vtt output/aligned.vtt -2000
2 番目のコマンドは transform is unfinished とともに非ゼロ状態を返し、ファイルを作ってはいけません。元字幕は 4000–7000 ms から始まり、編集後の緑のシーンは 2000 ms から始まります。必要なオフセットは引き続き、全キューの両端に -2000 です。
説明を読む前に、意図的に順不同で並べた次の操作について、自分で安全な順序を書きます。新しい出力先の作成、テキストの整形、全キューの解析、バイトの書き込み、指定入力の読み取り、全キューの移動です。後のキューが無効なテストファイルを読み、二番目のキューが失敗した場合に出力先が存在すべきか予想します。
cat data/bad-late.vtt
自分で考えた順序を、副作用を起こす境界と比較します。ファイル操作の境界は次の順序にします。まず、実際に指定された入力を UTF-8 テキストとして読みます。全キューを解析し、未対応または不正な内容をキューの文脈付きで拒否します。次に、オーバーフローを検査する算術でコレクション全体を移動し、負またはオーバーフローする時刻を拒否します。完全な結果をメモリ内で整形します。それらの操作がすべて成功した後で初めて、新しい出力を作り、準備済みのバイトを書きます。
完全な解析、シフト、整形より先に出力を作ると、後の失敗で空ファイルや部分ファイルが残る可能性があります。置き換えを許す書き込みは、有用な以前の結果を壊す可能性があります。OpenOptions はファイルの開き方を設定します。.write(true).create_new(true) は書き込み可能な新規ファイルを要求します。リンク先が存在しないシンボリックリンクも含め、パスが既に何らかのオブジェクトに占有されている場合、既存の内容を切り詰めずに開く操作が失敗します。この原子的な新規作成の判断は、存在確認と作成を別々に行う場合の競合を避けます。作成後に発生する無関係な I/O 障害まで含む、一般的なストレージトランザクションを保証するものではありません。
Path は借用したファイルシステムパスです。fs::read_to_string は完全な UTF-8 String または I/O エラーを返します。map_err は元のメッセージを保ちながら実際に失敗したパスを追加し、? は直ちにそのエラーを返します。提供された parse、shift_all、render は、メモリ上の完全なデータを扱います。Write は write_all を提供する標準トレイトで、このメソッドは準備したバッファーのすべてのバイトを書き込もうとします。インポートするとメソッドを使えるようになり、トレイトを実装する必要はありません。as_bytes は String の既存 UTF-8 バイトを借用し、文字単位のサイズでスライスしません。shifted.len() はキュー数で、書き込み成功後にだけ返します。
インポートも含めて、実際の起点のコードを読みます。
cat src/export.rs
現在は Path だけをインポートしています。他の名前を使えると思い込まないでください。ファイルシステムモジュールとビルダー用に use std::fs::{self, OpenOptions};、write_all を使えるようにするトレイト用に use std::io::Write;、準備済みのライブラリ関数用に use crate::{parse, render, shift_all}; を追加します。use std::path::Path; は残します。任意の参照にあるように、fs/OpenOptions、Write、Path、crate の関数の順に並べると分かりやすいですが、順序自体は採点対象ではありません。use グループの波かっこは複数の名前をインポートするもので、関数本体ではありません。
編集前に、許可されたソースだけのチェックポイントを作ります。mktemp -d は六個の X を置き換えて一意のディレクトリを作り、既存の名前は拒否します。$() はそのパスを、このシェルセッションの変数に取り込みます。引用符はパスを一まとまりに保ち、&& はディレクトリ作成成功時だけコピーします。cp -p は権限と変更時刻を保持してファイルをコピーします。これはソースのチェックポイントであり、出力字幕のバックアップではありません。変数を表示し、実際の名前を控えてください。接尾辞を推測してはいけません。
rs01_before_export=$(mktemp -d .edit-before-export.XXXXXX) && cp -p src/export.rs "$rs01_before_export/export.rs"
printf '%s\n' "$rs01_before_export"
printf は、このディレクトリ名とその後の改行を出力します。既存のソースを nano で開きます。-c はカーソル位置を表示します。
nano -c src/export.rs
Ctrl+W で pub fn transform を検索し、Enter を押します。型と戻り値のシグネチャは保ちます。_input、_output、_delta の先頭のアンダースコアを取り除いて名前を変えます。本体はこの関数の開き波かっこから最後の閉じ波かっこまでです。提供された時間モジュールではなく、この本体を変更します。
実行前に、安全な順序を一つの完全な関数として実装します。実際の input を raw に読み、cues に解析し、全キューを shifted に移動して、complete に整形します。失敗する可能性がある各入力操作では、.map_err(|error| format!("{}: {error}", input.display()))? で実際の入力パスを加えます。整形自体は Result ではなく String を返します。その後、OpenOptions::new().write(true).create_new(true).open(output) で変更可能な出力ファイルを設定し、失敗時には出力パス付きのエラーを加えます。最後に、file.write_all(complete.as_bytes()) が成功してから Ok(shifted.len()) を返します。どの式が最初に出力先を作成し得るのか、その時点ですべてのキューの検査が済んでいるのはなぜか、自分の言葉で説明してください。同じ公開動作を持つ同等の実装も認められます。
Ctrl+O、Enter、Ctrl+X で保存して nano を終了します。保存したソースを読みます。
cat src/export.rs
この境界を通して実際のソースを処理します。
cargo run --quiet -- shift data/original.vtt output/aligned.vtt -2000
Created output/aligned.vtt with 3 cues (offset -2000 ms)
cargo run --quiet -- inspect output/aligned.vtt
Cue 1: 2000 --> 5000 ms
Café 林地 🌿
Second line intact
Cue 2: 4000 --> 7000 ms
Overlap stays
Cue 3: 59750 --> 62250 ms
Across a minute
両方のテキスト行と重なる区間も含め、三つのキューがすべてあります。以前の試行で出力がすでに存在する場合は、そのまま保ちます。次のステップでは、この正確なファイル名を作り直す前に、上書きせずに復旧する手順を示します。
誤った編集をまだ保存していなければ、Ctrl+X を押し、保存の質問に N と答えるとファイルを変更せずに終了できます。保存済みのミスでは、Cargo が示したファイル名と行を読み、このソースを開き直して該当する式を修正します。自分のチェックポイントから復元する方法もあります。export チェックポイントは、この段階の前の状態を含みます。復元すると、この段階と、それ以降の関数をやり直す必要があります。
新しい Terminal を開いた場合、変数と作業ディレクトリは引き継がれません。まず、示された環境とディレクトリのコマンドを繰り返します。
export PATH=/home/labex/.cargo/bin:$PATH
export LANG=C.UTF-8 LC_ALL=C.UTF-8
cd /home/labex/project/subtitle-sync
以下の復旧コマンドは条件付きです。保存済みのミスを元に戻す場合だけ使います。ls -d はディレクトリ名自体を一覧表示し、* は生成された接尾辞に一致します。チェックポイントがなければ、そのエラーを読み、架空のパスを作らずにソースを修正します。
ls -d .edit-before-export.*
read は一行の入力を待ち、セッション変数に保存します。実行してから、上に表示された実際の完全なディレクトリ名を一つ入力または貼り付け、Enter を押します。例の接尾辞やドル記号を入力してはいけません。
read rs01_restore
復元前に、このファイルが期待する以前のソースであることを確認します。
cat "$rs01_restore/export.rs"
cp -p "$rs01_restore/export.rs" src/export.rs
その後 nano を開き直し、足りない実装をやり直して、失敗したコマンドを再実行します。setup を再実行したり、保護された入力ファイルを変更したりしないでください。コードの修正では、既存の出力 VTT や、ブラウザーの Blob に読み込み済みのファイルは置き換わりません。必要なパスを再生成する前に古い結果を保ち、その後、実際の出力を選び直して Load actual files をクリックします。
任意:完全な export.rs の比較・復旧用参照
この参照を開く前に、関数の編集を試して結果を確認します。これは完全な実装の一例であり、必須のコマンド手順ではありません。公開された契約の範囲内で同等のアルゴリズムが認められます。完成済みの作業を不必要に置き換えず、影響を受けた関数を比較します。
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::Path;
use crate::{parse, render, shift_all};
pub fn transform(input: &Path, output: &Path, delta: i64) -> Result<usize, String> {
let raw = fs::read_to_string(input)
.map_err(|error| format!("{}: {error}", input.display()))?;
let cues = parse(&raw)
.map_err(|error| format!("{}: {error}", input.display()))?;
let shifted = shift_all(&cues, delta)
.map_err(|error| format!("{}: {error}", input.display()))?;
let complete = render(&shifted);
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(output)
.map_err(|error| format!("{}: cannot create new output: {error}", output.display()))?;
file.write_all(complete.as_bytes())
.map_err(|error| format!("{}: write failed: {error}", output.display()))?;
Ok(shifted.len())
}
証拠を残してエラーから復旧する
このステップでは、ファイル競合、不正なキュー、無効なシフトを区別し、実際の動画再生を確認します。Terminal のプロジェクトディレクトリで続けます。次の失敗は意図的であり、非ゼロ状態になる必要があります。成功メッセージを出したり、指定した新規出力を作ったりしてはいけません。
まず、成功したコマンドを既存の結果に対して繰り返します。
cargo run --quiet -- shift data/original.vtt output/aligned.vtt -2000
既存の output/aligned.vtt を示すエラーを予期します。ファイルは変わりません。要求されたパスを作り直す前に古い結果を残すため、新しいバックアップディレクトリを作ります。mktemp -d は 6 個の X を一意の接尾辞に置き換え、既存ディレクトリの再利用を拒否します。$() はそのパスをセッション変数に取り込み、引用符で一まとまりに保ちます。&& はディレクトリ作成に成功したときだけ mv を実行し、mv は古い結果を新しい空のディレクトリへ移すので、別のバックアップを上書きしません。
rs01_backup_dir=$(mktemp -d output/aligned-backup.XXXXXX) &&
mv output/aligned.vtt "$rs01_backup_dir/aligned.vtt"
ディレクトリ作成に失敗すると移動は省略され、古い結果は残ります。再試行前にそのエラーを読んでください。現在の実装で、要求された正確な出力を再生成します。
cargo run --quiet -- shift data/original.vtt output/aligned.vtt -2000
次に、後のキューに不正な形式があるテストファイルを読みます。
cat data/bad-late.vtt
最初のキューは正しいものの、キュー 2 の終了時刻は開始時刻より前です。正しい先頭部分だけでは使えるエクスポートになりません。次のコマンドは非ゼロ状態で、実際のソース、キュー 2、その時間行を示す必要があります。
cargo run --quiet -- shift data/bad-late.vtt output/bad-result.vtt 0
指定パスを一覧表示する ls で、出力ができていないことを確認します。結果が存在しないため、このコマンドも非ゼロ状態になる想定です。
ls output/bad-result.vtt
有効なソースと別の新しい出力を選んで復旧します。
cargo run --quiet -- shift data/original.vtt output/after-bad-input.vtt -2000
パスの誤りは別種のエラーです。意図的に誤った複数形を使います。
cargo run --quiet -- shift data/originals.vtt output/after-path-error.vtt -2000
存在しない入力パスを示し、キューのエラーを作り出してはいけません。パスを修正します。
cargo run --quiet -- shift data/original.vtt output/after-path-error.vtt -2000
-4001 のオフセットは、最初の 4000 ms の開始時刻をゼロ未満にします。次のコマンドは、巨大な正数へ折り返したり切り詰めたりせず、このシフトを拒否する必要があります。
cargo run --quiet -- shift data/original.vtt output/negative-result.vtt -4001
巨大でも i64 として有効なオフセットは、端点に加えるとオーバーフローすることがあります。この失敗も、出力を作らず、原因の文脈が分かるエラーを返す必要があります。
cargo run --quiet -- shift data/original.vtt output/overflow-result.vtt 9223372036854775807
i64 の範囲外の数は、もっと前の引数解析で拒否されます。これも復旧可能なエラーであり、panic ではありません。
cargo run --quiet -- shift data/original.vtt output/invalid-offset.vtt 9223372036854775808
既知の有効なオフセットと、もう 1 つの新しいファイル名で復旧します。
cargo run --quiet -- shift data/original.vtt output/after-offset-errors.vtt -2000
これらの失敗後も、元の字幕と映像は残っています。i64 の極値ケースが検証するのは算術であり、巨大な時刻でのミリ秒精度のブラウザー再生ではありません。ここでのブラウザー観察は、この短編映像に対するものです。
CRLF 改行、同一開始時刻の重なるキュー、空白文字だけの本文、Unicode テキストを含む、別の有効なソースを調べます。
cargo run --quiet -- inspect data/whitespace.vtt
空白と 1 個のタブだけで構成される本文行があります。この行は空ではないため、キューの一部です。本当に空の行はキューを区切ります。本文が空の場合や、孤立したテキストの続きは未対応です。準備済み読み取り処理はこの違いを保ち、キューを黙って除去しません。この有効なケースを分の境界をまたいでエクスポートします。
cargo run --quiet -- shift data/whitespace.vtt output/whitespace-shifted.vtt 58001
Preview に切り替えます。Video は data/edited.webm のまま、Caption file に output/aligned.vtt を選び、Load actual files をクリックします。Time (seconds) に 2.5 を入力し、Go to time をクリックして、緑のシーンと完全な 2 行の Unicode 字幕を確認します。4.5 では重なる両キューが見え、60.5 では琥珀色のシーンに分の境界をまたぐキューが表示されるはずです。Play で実際の再生を確認してから一時停止します。読み込んだファイル名が実際の結果を識別します。ダウンロードボタンは読み込んだファイルを再構築せずに保存します。
output/whitespace-shifted.vtt を選択して 60.5 を確認することもできます。同一開始時刻の両キューが読み込まれますが、空白文字だけの字幕は見た目が空白になる場合があります。保存ファイルと読み込んだキューのテキストには、もう一方のキューの先頭/末尾の空白と 2 行目が保持されます。ただし、ブラウザーの配置が各空白やタブを区別できる幅で描画するとは限りません。正確な論理テキストは見た目の間隔から推測せず、ファイルから確認します。同一開始時刻のアクティブキューはブラウザー内で順序が異なることがありますが、出力ファイルのキュー順は保たれています。
Terminal に戻り、受け入れ対象の主な結果を最後にもう一度確認します。
cargo run --quiet -- inspect output/aligned.vtt
コマンドの綴りやパスが誤っている場合は、保護されたデータではなくコマンドを修正します。Rust コードが誤っていれば、ステップ 1 のソースだけのチェックポイント確認、または任意の比較用参照を使い、影響を受けた関数を修正してから再実行します。コード修正後に既存出力を作り直す場合は、先に一意のディレクトリへのバックアップで残してください。これで、成功したように見える部分ファイルではなく、完全な結果と過去の証拠を保持できます。
まとめ
全キューの検証と変換が完了した後に、ファイルへの副作用を起こす境界を実装しました。既存の出力パスは置き換えずに拒否し、後の不正データでも部分エクスポートを残さず、通常のパス、負の時刻、オーバーフローのエラーからは復旧できました。以前の作業を一意のバックアップディレクトリに残し、実際の Unicode、複数行、重なる字幕を本物のプレイヤーで検証しました。チャレンジでは、別の短編映像と異なる公開オフセットに、これらの既習の境界を組み合わせます。



