Ungültige Verschiebungen ohne Untertitelverlust ablehnen

RustBeginner
Jetzt üben

Einführung

Eine korrekte Untertitelumwandlung muss die gesamte Datei schützen, auch die Blöcke nach dem ersten richtigen Beispiel. Ein weiter hinten fehlerhafter Block darf kein Teilergebnis hinterlassen, und eine vorhandene Ausgabe darf nicht überschrieben werden, nur weil der neue Lauf erfolgreich aussieht.

Du implementierst in Rust die Dateigrenze um die vollständige Blockumwandlung und prüfst anschließend Unicode, mehrzeiligen Text, gültige Überlappung, ungültige Verschiebungen, fehlende Pfade und Ausgabekonflikte. Diese unabhängige Umgebung stellt die vollständigen Millisekunden- und Verschiebefunktionen des vorherigen Labs bereit. Es werden keine Dateien aus dessen VM benötigt. Der vorbereitete Player liest die tatsächlich ausgewählte Ausgabe; er baut keine Untertitel aus der ursprünglichen Quelle neu auf.

Du solltest Format und Zeitregeln des vorherigen Labs sowie dessen Rust-Voraussetzungen verstehen. Dieses Lab ergänzt die Grenze zum ausschließlichen Erstellen neuer Ausgaben; es vermittelt keine Videocodierung und behauptet keine Unterstützung sämtlicher WebVTT-Syntax. Der Autor schätzt 50 Minuten einschließlich Lesen und gewöhnlicher Wiederherstellung, nicht anhand einer mit Anfängern gemessenen Bearbeitungszeit.

Vor der Ausgabenerstellung alles prüfen

In diesem Schritt implementierst du die vollständige Abfolge aus Lesen, Prüfen und Erstellen einer neuen Datei. Öffne Terminal. Ergänze Cargo im Suchpfad dieser Shell, wähle die verfügbare UTF-8-Locale für die Sitzung und wechsle ins unabhängige Projekt:

export PATH=/home/labex/.cargo/bin:$PATH
export LANG=C.UTF-8 LC_ALL=C.UTF-8
cd /home/labex/project/subtitle-sync

Wiederhole die Exporte in jedem neuen Terminal und den Verzeichnisbefehl, wenn du deine Position verloren hast. Sie bearbeiten keine Startdateien. Unicode-Zeichen im Terminal können weiterhin als Platzhalter erscheinen; die tatsächlichen UTF-8-Dateibytes und Browseruntertitel erhalten den Text.

Bearbeite nur src/export.rs und behalte transform(input: &Path, output: &Path, delta: i64) -> Result<usize, String> bei. Lass jede andere Quelldatei, Cargo.toml, rust-toolchain.toml und alle Originaldateien unter data/ unverändert. Die vollständigen Zeitfunktionen sind bereits in src/time.rs vorhanden; ihre öffentlichen Vorbedingungen bleiben vom Parser geprüfte nicht negative Zeitfelder und gültige Cue-Objekte. Du darfst eine gleichwertige interne Abfolge implementieren, musst aber sämtliche Prüfungen vor der Ausgabenerstellung abschließen. Unveränderte vorbereitete Dateien behalten Bytes, Berechtigungen, Eigentümer/Gruppe und Änderungszeit; durch normales Lesen geänderte Zugriffszeiten sind ausgenommen.

Hier gilt dieselbe kontrollierte WebVTT-Teilmenge mit reinem Text wie im vorherigen Lab. Sie verlangt den exakten Header WEBVTT und eine leere Folgezeile. Zeitzeilen verwenden HH:MM:SS.mmm --> HH:MM:SS.mmm mit exakt den gezeigten Leerzeichen, ASCII-Ziffern, mindestens zwei Stundenziffern, zwei Minuten-/Sekundenziffern im Bereich 00–59 und drei Millisekundenziffern. Vollständige Zeiten müssen in nicht negative i64-Millisekunden passen. Starts dürfen nicht abnehmen; gleiche Starts und Überlappungen sind gültig; Enden liegen nach Starts. Ein Block enthält eine oder mehrere nicht leere logische Textzeilen. Unicode, Tabulatoren, mehrere Zeilen und führende/nachgestellte Leerzeichen bleiben erhalten, auch ausschließlich aus Leerraum bestehender Inhalt. Eine wirklich leere Zeile beendet den Block; verwaister Folgetext wird abgelehnt. Die Eingabe erlaubt LF/CRLF, einen optionalen abschließenden Zeilenumbruch und zusätzliche leere Trenner. Die vorbereitete Ausgabe verwendet LF, zwei Stundenziffern für 0–99 Stunden (darüber nur die zusätzlich erforderlichen Ziffern), eine leere Zeile nach dem Header und eine nach jedem Block einschließlich des letzten. Sie vereinheitlicht Zeitstempelschreibweise und Trenneranzahl, erhält aber den logischen Inhaltstext. Einzelne Wagenrückläufe, BOM/Header-Metadaten, Block-IDs/Einstellungen, NOTE/STYLE/REGION, Markup/Entitäten, die Inhaltszeichen <, > oder & sowie andere Steuerzeichen als Tabulatoren werden in diesem Kurs nicht unterstützt; die Eingabe wird abgelehnt, statt diese Inhalte stillschweigend zu entfernen.

Prüfe dieselbe tatsächliche Quelle und versuche danach absichtlich den unvollständigen Export:

cargo run --quiet -- inspect data/original.vtt
cargo run --quiet -- shift data/original.vtt output/aligned.vtt -2000

Der zweite Befehl sollte einen Status ungleich null mit transform is unfinished liefern; er darf keine Datei erstellen. Der ursprüngliche Untertitel beginnt bei 4000–7000 ms, die grüne Szene des geschnittenen Films bei 2000 ms. Die verlangte Verschiebung bleibt -2000 für jede Zeitgrenze jedes Blocks.

Notiere vor dem Lesen der Erklärung deine eigene sichere Reihenfolge für diese absichtlich ungeordneten Aktionen: ein neues Ziel erstellen, Text formatieren, alle Blöcke parsen, Bytes schreiben, die genannte Eingabe lesen, jeden Block verschieben. Lies die weiter hinten ungültige Testdatei und sage voraus, ob ein Ziel existieren sollte, wenn ihr zweiter Block fehlschlägt:

cat data/bad-late.vtt

Vergleiche deine Reihenfolge nun mit der Seiteneffektgrenze. Die Dateigrenze sollte diese Reihenfolge einhalten: Lies zuerst die tatsächlich genannte Eingabe als UTF-8-Text. Parse alle Blöcke und lehne nicht unterstützten oder fehlerhaften Inhalt mit Blockkontext ab. Verschiebe dann die gesamte Sammlung mit Überlaufprüfung und lehne negative oder überlaufende Zeiten ab. Formatiere das vollständige Ergebnis im Speicher. Erst nach Erfolg all dieser Operationen erstellst du eine neue Ausgabe und schreibst die vorbereiteten Bytes.

Eine Ausgabe vor vollständigem Parsen, Verschieben und Formatieren zu erstellen könnte bei einem späteren Fehler eine leere oder teilweise Datei hinterlassen. Ersetzendes Schreiben könnte ein nützliches früheres Ergebnis zerstören. OpenOptions bestimmt, wie eine Datei geöffnet wird. .write(true).create_new(true) fordert eine neue beschreibbare Datei an; belegt bereits irgendein Objekt den Pfad, einschließlich eines symbolischen Links ohne vorhandenes Ziel, schlägt das Öffnen fehl, ohne es abzuschneiden. Diese atomare Entscheidung zur ausschließlichen Neuerstellung vermeidet ein Zeitfenster zwischen getrennter Existenzprüfung und Erstellung. Sie ist keine allgemeine Speichertransaktionsgarantie für einen unabhängigen E/A-Fehler nach der Erstellung.

Path ist ein ausgeliehener Dateisystempfad. fs::read_to_string liefert den vollständigen UTF-8-String oder einen E/A-Fehler. map_err ergänzt den tatsächlich fehlgeschlagenen Pfad und behält die Meldung bei; ? gibt den Fehler sofort zurück. Die bereitgestellten Funktionen parse, shift_all und render arbeiten mit vollständigen Daten im Speicher. Write ist der Standard-Trait, der write_all bereitstellt; die Methode versucht, jedes Byte des vorbereiteten Puffers zu schreiben. Der Import macht die Methode verfügbar; du musst keinen Trait implementieren. as_bytes leiht die vorhandenen UTF-8-Bytes des Strings aus, ohne Stücke in Zeichengröße abzutrennen. shifted.len() ist die Blockanzahl und wird erst nach erfolgreichem Schreiben zurückgegeben.

Lies den tatsächlichen Starter einschließlich seiner Imports:

cat src/export.rs

Derzeit importiert er nur Path. Setze nicht voraus, dass die anderen Namen verfügbar sind. Ergänze use std::fs::{self, OpenOptions}; für Dateisystemmodul und Builder, use std::io::Write; für den Trait, der write_all verfügbar macht, und use crate::{parse, render, shift_all}; für die vorbereiteten Bibliotheksfunktionen. Erhalte use std::path::Path;. Eine klare Importreihenfolge ist fs/OpenOptions, Write, Path und dann die Crate-Funktionen wie in der optionalen Referenz; die Reihenfolge selbst wird nicht bewertet. Die Klammern einer use-Gruppe importieren mehrere Namen; sie sind kein Funktionsrumpf.

Erstelle vor dem Bearbeiten einen Sicherungsstand nur der erlaubten Quelldatei. mktemp -d erstellt durch Ersetzen der sechs X-Zeichen ein eindeutiges Verzeichnis; ein vorhandener Name wird abgelehnt. $() übernimmt diesen Pfad in eine Variable für diese Shell-Sitzung. Anführungszeichen halten den Pfad zusammen, und && kopiert erst nach erfolgreicher Verzeichniserstellung. cp -p kopiert die Datei unter Erhaltung ihrer Berechtigungen und Änderungszeit. Dies ist ein Quelltext-Sicherungsstand, keine Sicherung einer Untertitelausgabe. Gib die Variable aus und bewahre den tatsächlichen Namen auf; rate das Suffix nicht.

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 gibt diesen Verzeichnisnamen mit anschließendem Zeilenumbruch aus. Öffne den vorhandenen Quelltext mit nano; -c zeigt die Cursorposition:

nano -c src/export.rs

Suche mit Ctrl+W nach pub fn transform, dann Enter. Erhalte Typen und Rückgabesignatur. Benenne _input, _output und _delta um, indem du ihre führenden Unterstriche entfernst. Der Rumpf liegt zwischen der öffnenden Klammer dieser Funktion und der letzten schließenden Klammer; ändere diesen Rumpf, nicht das bereitgestellte Zeitmodul.

Implementiere deine sichere Abfolge als vollständige Funktion, bevor du sie ausführst. Lies die tatsächliche input in raw, parse sie in cues, verschiebe alle Blöcke in shifted und formatiere complete. Ergänze für jede fehlbare Eingabeoperation den tatsächlichen Eingabepfad mit .map_err(|error| format!("{}: {error}", input.display()))?. Das Formatieren selbst gibt einen String zurück, keinen Result-Wert. Konfiguriere dann eine veränderbare Ausgabedatei über OpenOptions::new().write(true).create_new(true).open(output) und ergänze bei Fehler den Ausgabepfad. Schließlich muss file.write_all(complete.as_bytes()) vor Ok(shifted.len()) erfolgreich sein. Erkläre dir selbst, welcher erste Ausdruck ein Ziel erzeugen kann und warum zu diesem Zeitpunkt bereits jeder Block geprüft ist. Gleichwertige Implementierungen mit demselben öffentlichen Verhalten sind erlaubt.

Verwende Ctrl+O, Enter, Ctrl+X, um zu speichern und nano zu verlassen. Lies den gespeicherten Quelltext:

cat src/export.rs

Verarbeite die tatsächliche Quelle über diese Grenze:

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

Alle drei Blöcke sind vorhanden, einschließlich beider Textzeilen und der überlappenden Intervalle. Falls die Ausgabe nach einem früheren Versuch bereits existiert, bewahre sie: Der nächste Schritt zeigt die Wiederherstellung ohne Überschreiben, bevor du diesen exakten Dateinamen erneut erstellst.

Wenn du eine versehentliche Änderung noch nicht gespeichert hast, drücke Ctrl+X und dann N bei der Speicherfrage, um die Datei unverändert zu lassen. Lies bei einem gespeicherten Fehler Cargos genannten Dateinamen und die Zeile, öffne diesen Quelltext erneut und korrigiere den lokalen Ausdruck. Alternativ stelle deinen eigenen Sicherungsstand wieder her. Der Sicherungsstand export enthält den Zustand vor dieser Phase; nach dem Wiederherstellen musst du diese Phase und alle späteren Funktionen erneut implementieren.

Wenn du ein neues Terminal geöffnet hast, werden seine Variablen und sein Arbeitsverzeichnis nicht übernommen. Wiederhole zuerst die sichtbaren Umgebungs- und Verzeichnisbefehle:

export PATH=/home/labex/.cargo/bin:$PATH
export LANG=C.UTF-8 LC_ALL=C.UTF-8
cd /home/labex/project/subtitle-sync

Die folgenden Wiederherstellungsbefehle sind bedingt: Verwende sie nur, wenn du einen gespeicherten Fehler rückgängig machst. ls -d listet die Verzeichnisnamen selbst auf; * passt auf ihre erzeugten Suffixe. Existiert kein Sicherungsstand, lies diesen Fehler und repariere den Quelltext, statt einen Pfad zu erfinden.

ls -d .edit-before-export.*

read wartet auf eine Zeile und speichert sie in einer Sitzungsvariable. Führe es aus, tippe oder füge dann einen tatsächlichen vollständigen oben ausgegebenen Verzeichnisnamen ein und drücke Enter. Tippe weder ein Beispielsuffix noch das Dollarzeichen:

read rs01_restore

Prüfe vor dem Wiederherstellen, dass die Datei den erwarteten früheren Quelltext enthält:

cat "$rs01_restore/export.rs"
cp -p "$rs01_restore/export.rs" src/export.rs

Öffne danach nano erneut, ergänze die fehlende Implementierung und wiederhole den fehlgeschlagenen Befehl. Führe das Setup nicht erneut aus und ändere keine geschützten Eingabedateien. Eine Codekorrektur ersetzt weder eine vorhandene Ausgabe-VTT noch die bereits in einen Browser-Blob geladene Datei; bewahre das alte Ergebnis vor dem Neuerzeugen seines geforderten Pfads und wähle dann diese tatsächliche Ausgabe aus und klicke erneut auf Load actual files.

Optionale vollständige export.rs-Vergleichs- und Wiederherstellungsreferenz

Versuche die Funktionsänderungen und prüfe ihre Ergebnisse, bevor du diese Referenz öffnest. Sie zeigt eine vollständige Implementierung, keine vorgeschriebene Befehlsfolge. Gleichwertige Algorithmen sind im Rahmen des öffentlichen Vertrags erlaubt. Vergleiche die betroffene Funktion, statt bereits fertige Arbeit unnötig zu ersetzen.

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())
}

Fehler beheben und dabei Nachweise erhalten

In diesem Schritt unterscheidest du Dateikonflikte, fehlerhafte Blöcke und ungültige Verschiebungen und bestätigst anschließend echte Videowiedergabe. Arbeite in Terminal im Projektverzeichnis weiter. Die folgenden Fehler sind beabsichtigt und müssen einen Status ungleich null liefern; sie dürfen weder Erfolg melden noch die angeforderte neue Ausgabe erstellen.

Wiederhole zuerst den erfolgreichen Befehl mit seinem vorhandenen Ergebnis:

cargo run --quiet -- shift data/original.vtt output/aligned.vtt -2000

Erwarte einen Fehler, der die vorhandene Datei output/aligned.vtt nennt. Sie bleibt unverändert. Um ein früheres Ergebnis vor dem Neuaufbau des geforderten Pfads zu erhalten, erstelle ein neues Sicherungsverzeichnis. mktemp -d ersetzt die sechs X-Zeichen durch ein eindeutiges Suffix und verwendet kein vorhandenes Verzeichnis erneut. $() übernimmt diesen Pfad in eine Sitzungsvariable; Anführungszeichen halten ihn zusammen. && führt mv nur nach erfolgreicher Verzeichniserstellung aus; mv verschiebt das alte Ergebnis in das neue leere Verzeichnis, sodass keine andere Sicherung überschrieben wird.

rs01_backup_dir=$(mktemp -d output/aligned-backup.XXXXXX) &&
mv output/aligned.vtt "$rs01_backup_dir/aligned.vtt"

Scheitert die Verzeichniserstellung, entfällt das Verschieben und das alte Ergebnis bleibt erhalten. Lies vor dem Wiederholen diesen Fehler. Erzeuge die exakt verlangte Ausgabe mit deiner aktuellen Implementierung erneut:

cargo run --quiet -- shift data/original.vtt output/aligned.vtt -2000

Lies nun die Testdatei mit einem weiter hinten fehlerhaften Block:

cat data/bad-late.vtt

Ihr erster Block ist korrekt, doch Block 2 endet vor seinem Start. Ein Teilpräfix ist kein verwendbarer Export. Der folgende Befehl muss einen Status ungleich null liefern und die tatsächliche Quelle, Block 2 und dessen Zeitzeile identifizieren:

cargo run --quiet -- shift data/bad-late.vtt output/bad-result.vtt 0

Prüfe mit ls, das einen angegebenen Pfad auflistet, dass keine Ausgabe entstanden ist. Auch dieser Befehl muss einen Status ungleich null liefern, weil das Ergebnis nicht existiert:

ls output/bad-result.vtt

Verwende zur Wiederherstellung die gültige Quelle und eine andere neue Ausgabe:

cargo run --quiet -- shift data/original.vtt output/after-bad-input.vtt -2000

Ein falscher Pfad ist ein anderer Fehler. Verwende absichtlich einen falschen Plural:

cargo run --quiet -- shift data/originals.vtt output/after-path-error.vtt -2000

Die Meldung sollte den fehlenden Eingabepfad nennen und keinen Blockfehler erfinden. Korrigiere den Pfad:

cargo run --quiet -- shift data/original.vtt output/after-path-error.vtt -2000

Ein Versatz von -4001 würde die erste Startzeit von 4000 ms unter null verschieben. Der nächste Befehl muss dies ablehnen, statt durch Überlauf eine riesige positive Zahl zu erzeugen oder den Zeitwert zu begrenzen:

cargo run --quiet -- shift data/original.vtt output/negative-result.vtt -4001

Ein sehr großer, aber gültiger i64-Versatz kann beim Addieren zu einer Zeitgrenze überlaufen. Auch dieser Fehler muss mit zuordenbarem Kontext zurückgegeben werden, ohne eine Ausgabe zu erzeugen:

cargo run --quiet -- shift data/original.vtt output/overflow-result.vtt 9223372036854775807

Eine Zahl außerhalb von i64 wird noch früher von der Argumentverarbeitung abgelehnt. Auch das ist ein behandelbarer Fehler und kein Panic:

cargo run --quiet -- shift data/original.vtt output/invalid-offset.vtt 9223372036854775808

Nutze zur Wiederherstellung den bekannten gültigen Versatz und einen weiteren neuen Dateinamen:

cargo run --quiet -- shift data/original.vtt output/after-offset-errors.vtt -2000

Nach all diesen Fehlern sind die ursprünglichen Untertitel und der Film weiterhin vorhanden. Die i64-Grenzfälle prüfen Arithmetik und keine millisekundengenaue Browserwiedergabe bei riesigen Zeitwerten; die Browserbeobachtungen beziehen sich auf diesen Kurzfilm.

Untersuche eine weitere gültige Quelle mit CRLF-Zeilenenden, überlappenden Blöcken mit gleichem Start, einem nur aus Leerraum bestehenden Inhalt und Unicode-Text:

cargo run --quiet -- inspect data/whitespace.vtt

Sie enthält eine Inhaltszeile ausschließlich aus Leerzeichen und einem Tabulator. Diese Zeile ist nicht leer und gehört daher zu ihrem Block. Eine wirklich leere Zeile trennt dagegen Blöcke; leerer Inhalt oder eine verwaiste Textfortsetzung wird nicht unterstützt. Der vorbereitete Leser bewahrt diese Unterscheidung und filtert nie stillschweigend einen Block heraus. Exportiere den gültigen Fall über einen Minutenübergang hinweg:

cargo run --quiet -- shift data/whitespace.vtt output/whitespace-shifted.vtt 58001

Wechsle zu Preview. Lass Video auf data/edited.webm, wähle Caption file output/aligned.vtt und klicke auf Load actual files. Gib unter Time (seconds) 2.5 ein, klicke auf Go to time und achte auf die grüne Szene und den vollständigen zweizeiligen Unicode-Untertitel. Bei 4.5 sollten beide überlappenden Blöcke sichtbar sein; bei 60.5 sollte die bernsteinfarbene Szene den Block am Minutenübergang zeigen. Bestätige echte Wiedergabe mit Play und pausiere danach. Der geladene Dateiname kennzeichnet dein tatsächliches Ergebnis. Die Downloadschaltflächen speichern die geladenen Dateien, ohne sie neu aufzubauen.

Du kannst auch output/whitespace-shifted.vtt auswählen und 60.5 prüfen. Beide Blöcke mit gleichem Start werden geladen; der ausschließlich aus Leerraum bestehende Untertitel kann optisch leer sein. Die gespeicherte Datei und der geladene Blocktext erhalten die führenden/nachgestellten Leerzeichen und die zweite Zeile des anderen Blocks, auch wenn das Browserlayout nicht jedes Leerzeichen oder jeden Tabulator als unterscheidbare Breite darstellt. Den exakten logischen Text prüfst du anhand der Datei statt anhand sichtbarer Abstände. Der Browser kann aktive Blöcke mit gleichem Start anders anordnen; die Reihenfolge in der Ausgabedatei bleibt dennoch erhalten.

Kehre zu Terminal zurück und prüfe ein letztes Mal das akzeptierte Hauptergebnis:

cargo run --quiet -- inspect output/aligned.vtt

Enthält ein Befehl einen Tipp- oder Pfadfehler, korrigiere den Befehl statt geschützter Daten. Ist dein Rust-Code falsch, verwende die Bestätigung des reinen Quelltext-Sicherungsstands oder die optionale Vergleichsreferenz aus Schritt 1, um die betroffene Funktion zu reparieren, und wiederhole dann den Aufruf. Soll nach einer Codekorrektur eine vorhandene Ausgabe neu entstehen, sichere sie zunächst im eindeutigen Verzeichnis. Du hast nun ein vollständiges Ergebnis samt früheren Nachweisen statt einer nur scheinbar erfolgreichen Teildatei.

Zusammenfassung

Du hast die Grenze für Dateiseiteneffekte nach vollständiger Prüfung und Umwandlung der Blöcke implementiert. Bestehende Ausgabepfade wurden ohne Ersetzung abgelehnt, weiter hinten fehlerhafte Daten erzeugten keinen Teilexport, und gewöhnliche Pfad-, negative Zeit- und Überlauffehler blieben behandelbar. Du hast frühere Arbeit in eindeutigen Sicherungsverzeichnissen erhalten und tatsächliche Unicode-, mehrzeilige und überlappende Untertitel mit dem echten Player geprüft. Die Challenge kombiniert diese erlernten Grenzen für einen anderen Kurzfilm mit einem anderen öffentlich vorgegebenen Versatz.