Refuser les décalages invalides sans perdre les sous-titres

RustBeginner
Pratiquer maintenant

Introduction

Une transformation correcte des sous-titres doit protéger tout le fichier, y compris les blocs qui suivent le premier exemple correct. Un bloc mal formé situé plus loin dans le fichier ne doit pas laisser de résultat partiel, et une sortie existante ne doit pas être écrasée simplement parce que la nouvelle exécution semble réussir.

Vous implémenterez en Rust la frontière sur les fichiers autour de la transformation complète des blocs, puis vérifierez Unicode, le texte multiligne, les chevauchements légitimes, les décalages invalides, les chemins manquants et les conflits de sortie. Cet environnement indépendant fournit les fonctions complètes de conversion en millisecondes et de décalage du laboratoire précédent. Aucun fichier de sa VM n’est nécessaire. Le lecteur préparé lit la sortie réellement sélectionnée ; il ne reconstruit pas les sous-titres depuis la source originale.

Vous devez comprendre le format et les règles temporelles du laboratoire précédent, ainsi que ses prérequis Rust. Ce laboratoire ajoute la frontière de création d’une nouvelle sortie ; il n’enseigne pas l’encodage vidéo et ne prétend pas prendre en charge toute la syntaxe WebVTT. L’auteur estime la durée à 50 minutes, lecture et récupération courante comprises, et non d’après un temps mesuré auprès de débutants.

Tout valider avant de créer la sortie

Dans cette étape, vous implémenterez la séquence complète de lecture, validation et création d’un nouveau fichier. Ouvrez Terminal. Ajoutez Cargo au chemin de ce shell, choisissez la locale UTF-8 disponible pour la session et entrez dans le projet indépendant :

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

Répétez les exports dans tout nouveau terminal et la commande de changement de répertoire si vous ne savez plus où vous êtes. Ils ne modifient aucun fichier de démarrage. Les glyphes Unicode du terminal peuvent encore apparaître comme des symboles de remplacement ; les vrais octets UTF-8 du fichier et les sous-titres du navigateur conservent leur texte.

Modifiez uniquement src/export.rs, en conservant transform(input: &Path, output: &Path, delta: i64) -> Result<usize, String>. Gardez inchangés tous les autres fichiers source, Cargo.toml, rust-toolchain.toml et les originaux sous data/. Les fonctions temporelles complètes sont déjà présentes dans src/time.rs ; leurs préconditions publiques restent des champs temporels non négatifs validés par l’analyseur et des objets Cue valides. Vous pouvez implémenter une séquence interne équivalente, mais toute validation doit être terminée avant la création de la sortie. Les fichiers préparés laissés inchangés conservent leurs octets, permissions, propriétaire/groupe et date de modification ; la date d’accès due aux lectures ordinaires est exclue.

Le sous-ensemble WebVTT contrôlé en texte brut du laboratoire précédent s’applique ici. Il exige l’en-tête exact WEBVTT et une ligne suivante vide. Les lignes d’horodatage utilisent HH:MM:SS.mmm --> HH:MM:SS.mmm avec les espaces exactes indiquées, des chiffres ASCII, au moins deux chiffres d’heures, deux de minutes/secondes dans 00–59 et trois de millisecondes. Les temps complets doivent tenir dans des millisecondes i64 non négatives. Les débuts sont non décroissants ; les débuts égaux et les chevauchements sont autorisés ; les fins dépassent les débuts. Un bloc possède une ou plusieurs lignes logiques de contenu non vides. Unicode, tabulations, texte multiligne et espaces de début/fin sont conservés, y compris les contenus composés uniquement de caractères blancs. Une ligne véritablement vide termine le bloc ; le texte orphelin qui suit est rejeté. L’entrée accepte LF/CRLF, une fin de ligne finale facultative et des séparateurs vides supplémentaires. La sortie préparée utilise LF, deux chiffres d’heures pour 0–99 heures (uniquement les chiffres supplémentaires nécessaires au-delà), une ligne vide après l’en-tête et une après chaque bloc, y compris le dernier. Elle normalise l’écriture des horodatages et le nombre de séparateurs tout en conservant le texte logique du contenu. Ce cours ne prend pas en charge les retours chariot isolés, BOM/métadonnées d’en-tête, identifiants/paramètres de bloc, NOTE/STYLE/REGION, balisage/entités, caractères de contenu <, > ou &, ni les caractères de contrôle autres que les tabulations ; ils sont rejetés plutôt que supprimés silencieusement.

Examinez la même source réelle, puis essayez volontairement l’exportation inachevée :

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

La deuxième commande doit renvoyer un état non nul avec transform is unfinished ; elle ne doit créer aucun fichier. Le sous-titre original commence à 4000–7000 ms, et la scène verte du film monté commence à 2000 ms. Le décalage requis reste -2000 pour chaque extrémité de chaque bloc.

Avant de lire l’explication, notez votre propre ordre sûr pour ces actions volontairement mélangées : créer une nouvelle cible, mettre le texte en forme, analyser tous les blocs, écrire les octets, lire l’entrée nommée, décaler chaque bloc. Lisez les données de test avec un bloc tardif invalide et prévoyez si une cible doit exister lorsque le deuxième bloc échoue :

cat data/bad-late.vtt

Comparez maintenant votre ordre à la frontière des effets sur les fichiers. La frontière sur les fichiers doit suivre cet ordre. Lisez d’abord l’entrée réellement nommée comme texte UTF-8. Analysez tous les blocs et rejetez le contenu non pris en charge ou mal formé avec son contexte de bloc. Décalez ensuite toute la collection par une arithmétique vérifiée, en rejetant les temps négatifs ou débordants. Préparez le résultat complet en mémoire. Ce n’est qu’après la réussite de toutes ces opérations que vous créez une nouvelle sortie et écrivez ces octets préparés.

Créer la sortie avant l’analyse, le décalage et la mise en forme complets pourrait laisser un fichier vide ou partiel après un échec tardif. Une écriture avec remplacement pourrait détruire un ancien résultat utile. OpenOptions configure la manière d’ouvrir un fichier. .write(true).create_new(true) demande un nouveau fichier accessible en écriture ; si un objet occupe déjà le chemin, y compris un lien symbolique dont la cible n’existe pas, l’ouverture échoue sans le tronquer. Cette décision atomique de création exclusive évite une condition de concurrence entre une vérification d’existence et une création effectuées séparément. Elle ne garantit pas une transaction générale de stockage en cas de panne d’E/S indépendante survenant après la création.

Path est un chemin emprunté du système de fichiers. fs::read_to_string renvoie le String UTF-8 complet ou une erreur d’E/S. map_err ajoute le vrai chemin en échec tout en conservant son message ; ? renvoie cette erreur immédiatement. Les fonctions fournies parse, shift_all et render travaillent sur des données complètes en mémoire. Write est le trait standard qui fournit write_all, méthode qui tente d’écrire chaque octet du tampon préparé. Son import rend cette méthode disponible ; il n’est pas nécessaire d’implémenter un trait. as_bytes emprunte les octets UTF-8 existants du String sans les découper en tranches de la taille d’un caractère. shifted.len() est le nombre de blocs, renvoyé seulement après la réussite de l’écriture.

Lisez le vrai code de départ, y compris ses imports :

cat src/export.rs

Il importe actuellement uniquement Path. Ne supposez pas que les autres noms sont disponibles. Ajoutez use std::fs::{self, OpenOptions}; pour le module de système de fichiers et le constructeur, use std::io::Write; pour le trait qui rend write_all disponible et use crate::{parse, render, shift_all}; pour les fonctions préparées de la bibliothèque. Gardez use std::path::Path;. Un ordre d’import clair est fs/OpenOptions, Write, Path, puis les fonctions de la bibliothèque actuelle, comme dans la référence facultative ; l’ordre lui-même n’est pas évalué. Les accolades d’un groupe use importent plusieurs noms ; elles ne délimitent pas un corps de fonction.

Avant toute modification, créez un point de restauration contenant uniquement le code source autorisé. mktemp -d crée un répertoire unique en remplaçant les six caractères X ; il refuse un nom existant. $() capture ce chemin dans une variable de cette session shell. Les guillemets gardent le chemin en un seul morceau et && copie seulement après la réussite de la création du répertoire. cp -p copie le fichier en conservant ses permissions et sa date de modification. Il s’agit d’un point de restauration du code source, pas d’une sauvegarde des sous-titres produits. Affichez la variable et gardez le vrai nom ; ne devinez pas le suffixe.

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 affiche ce nom de répertoire suivi d’un saut de ligne. Ouvrez le code source existant avec nano ; -c affiche la position du curseur :

nano -c src/export.rs

Recherchez pub fn transform avec Ctrl+W, puis Entrée. Gardez ses types et sa signature de retour. Renommez _input, _output et _delta en supprimant leurs traits de soulignement initiaux. Le corps se trouve entre l’accolade ouvrante de cette fonction et sa dernière accolade fermante ; modifiez ce corps, pas le module temporel fourni.

Implémentez votre séquence sûre comme une fonction complète avant de l’exécuter. Lisez le vrai input dans raw, analysez-le en cues, décalez tous les blocs dans shifted et mettez en forme complete. Pour chaque opération d’entrée pouvant échouer, ajoutez le vrai chemin d’entrée avec .map_err(|error| format!("{}: {error}", input.display()))?. La mise en forme renvoie elle-même un String, pas Result. Configurez ensuite un fichier de sortie mutable avec OpenOptions::new().write(true).create_new(true).open(output), en ajoutant une erreur de chemin de sortie en cas d’échec. Enfin, file.write_all(complete.as_bytes()) doit réussir avant Ok(shifted.len()). Expliquez-vous quelle est la première expression pouvant créer une cible et pourquoi chaque bloc a déjà été vérifié à ce moment-là. Les implémentations équivalentes ayant le même comportement public sont autorisées.

Utilisez Ctrl+O, Entrée, Ctrl+X pour enregistrer et quitter nano. Lisez le code source enregistré :

cat src/export.rs

Faites passer la vraie source par cette frontière :

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

Les trois blocs sont présents, y compris les deux lignes de texte et les intervalles qui se chevauchent. Si la sortie existe après une tentative antérieure, conservez-la : l’étape suivante propose une reprise sans écrasement avant de recréer ce nom exact.

Si vous n’avez pas enregistré une modification accidentelle, appuyez sur Ctrl+X, puis sur N à la question d’enregistrement pour laisser le fichier inchangé. Pour une erreur enregistrée, lisez le fichier et la ligne indiqués par Cargo, rouvrez ce fichier source et corrigez l’expression locale. Vous pouvez aussi restaurer votre propre point de restauration. Le point de restauration export contient l’état avant cette phase ; le restaurer implique de refaire cette phase et les fonctions suivantes.

Si vous avez ouvert un nouveau Terminal, ses variables et son répertoire courant ne sont pas conservés. Répétez d’abord les commandes visibles d’environnement et de répertoire :

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

Les commandes de récupération suivantes sont conditionnelles : utilisez-les uniquement pour annuler une erreur enregistrée. ls -d liste les noms des répertoires eux-mêmes ; * correspond à leurs suffixes générés. Si aucun point de restauration n’existe, lisez cette erreur et corrigez le code source au lieu d’inventer un chemin.

ls -d .edit-before-export.*

read attend une ligne et la stocke dans une variable de session. Exécutez-le, puis saisissez ou collez un vrai nom complet de répertoire affiché ci-dessus et appuyez sur Entrée. Ne saisissez pas un suffixe d’exemple ni le signe dollar :

read rs01_restore

Confirmez que le fichier contient bien le code source antérieur attendu avant de le restaurer :

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

Rouvrez ensuite nano, refaites l’implémentation manquante et relancez la commande ayant échoué. Ne relancez pas la préparation de l’environnement et ne modifiez pas les fichiers d’entrée protégés. Corriger le code ne remplace ni un VTT de sortie existant ni le fichier déjà chargé dans un Blob du navigateur ; conservez l’ancien résultat avant de régénérer son chemin requis, puis sélectionnez cette vraie sortie et cliquez à nouveau sur Load actual files.

Référence complète facultative de export.rs pour comparaison et récupération

Essayez les modifications de fonctions et examinez leurs résultats avant d’ouvrir cette référence. Elle montre une implémentation complète, pas une suite de commandes obligatoire. Les algorithmes équivalents sont autorisés dans le cadre du contrat public. Comparez la fonction concernée plutôt que de remplacer inutilement du travail déjà terminé.

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

Récupérer après les erreurs en conservant les preuves

Dans cette étape, vous distinguerez les conflits de fichiers, les blocs mal formés et les décalages invalides, puis confirmerez la vraie lecture vidéo. Continuez dans Terminal, dans le répertoire du projet. Les échecs ci-dessous sont volontaires et doivent renvoyer un état non nul ; ils ne doivent ni afficher de message de réussite ni créer la nouvelle sortie demandée.

Répétez d’abord la commande réussie sur son résultat existant :

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

Attendez-vous à une erreur qui nomme output/aligned.vtt, déjà présent. Il reste inchangé. Pour préserver un ancien résultat avant de reconstruire le chemin requis, créez un nouveau répertoire de sauvegarde. mktemp -d remplace les six caractères X par un suffixe unique et refuse de réutiliser un répertoire existant. $() récupère ce chemin dans une variable de session et les guillemets le gardent en un seul morceau. && exécute mv seulement si la création du répertoire réussit ; mv déplace l’ancien résultat dans ce nouveau répertoire vide, sans écraser une autre sauvegarde.

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

Si la création du répertoire échoue, le déplacement est ignoré et l’ancien résultat reste en place. Lisez cette erreur avant de réessayer. Régénérez la sortie exacte requise avec votre implémentation actuelle :

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

Lisez maintenant les données de test comportant un bloc tardif mal formé :

cat data/bad-late.vtt

Le premier bloc est correct, mais le bloc 2 se termine avant de commencer. Un préfixe partiel n’est pas une exportation utilisable. La commande suivante doit renvoyer un état non nul et identifier la vraie source, le bloc 2 et sa ligne d’horodatage :

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

Utilisez ls, qui liste un chemin nommé, pour vérifier qu’aucune sortie n’est apparue. Cette commande doit également renvoyer un état non nul puisque le résultat n’existe pas :

ls output/bad-result.vtt

Reprenez avec la source valide et une autre nouvelle sortie :

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

Un mauvais chemin est une erreur différente. Introduisez volontairement une faute au pluriel :

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

L’erreur doit nommer le chemin d’entrée manquant, sans inventer d’erreur de bloc. Corrigez le chemin :

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

Un décalage -4001 ferait passer le premier début de 4000 ms sous zéro. La commande suivante doit refuser ce décalage au lieu de produire par débordement un énorme nombre positif ou de le ramener à une limite :

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

Un décalage énorme mais valide en i64 peut déborder lorsqu’il est ajouté à une extrémité. Cet échec doit lui aussi renvoyer une erreur avec son contexte sans créer de sortie :

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

Un nombre hors d’i64 est rejeté encore plus tôt par l’analyse des arguments. Il s’agit toujours d’une erreur récupérable, pas d’un panic :

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

Reprenez avec le décalage valide connu et un autre nouveau nom de fichier :

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

Tous ces échecs ont laissé disponibles les sous-titres et le film originaux. Les cas extrêmes d’i64 vérifient l’arithmétique, pas une lecture précise à la milliseconde dans le navigateur pour des temps gigantesques ; les observations du navigateur concernent ici ce court métrage.

Examinez une autre source valide comportant des fins de ligne CRLF, des blocs de même début qui se chevauchent, un contenu uniquement composé de caractères blancs et du texte Unicode :

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

Elle contient une ligne composée uniquement d’espaces et d’une tabulation. Cette ligne n’est pas vide et appartient donc à son bloc. Une ligne véritablement vide sépare au contraire les blocs ; un contenu vide ou une continuation de texte orpheline n’est pas pris en charge. Le lecteur préparé conserve cette distinction et ne filtre jamais silencieusement un bloc. Exportez le cas valide en franchissant une limite de minute :

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

Passez à Preview. Gardez Video sur data/edited.webm, sélectionnez Caption file output/aligned.vtt et cliquez sur Load actual files. Dans Time (seconds), entrez 2.5, cliquez sur Go to time et repérez la scène verte et le sous-titre Unicode complet de deux lignes. À 4.5, les deux blocs qui se chevauchent doivent être visibles ; à 60.5, la scène ambrée doit afficher le bloc du changement de minute. Cliquez sur Play pour confirmer la vraie lecture, puis mettez en pause. Le nom chargé identifie votre vrai résultat. Les boutons de téléchargement enregistrent les fichiers chargés sans les reconstruire.

Vous pouvez aussi sélectionner output/whitespace-shifted.vtt et examiner 60.5. Les deux blocs de même début sont chargés ; le sous-titre constitué entièrement de caractères blancs peut paraître vide. Le fichier enregistré et le texte du bloc chargé conservent les espaces de début/fin et la seconde ligne de l’autre bloc, même si la mise en page du navigateur ne représente pas chaque espace ou tabulation par une largeur discernable. Le texte logique exact se vérifie depuis le fichier, plutôt qu’en le déduisant de l’espacement visuel. Le navigateur peut ordonner différemment les blocs actifs de même début ; l’ordre des blocs du fichier de sortie reste conservé.

Revenez à Terminal et examinez une dernière fois le résultat principal accepté :

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

Si une commande contient une faute de frappe ou de chemin, corrigez-la au lieu de modifier les données protégées. Si votre code Rust est incorrect, utilisez la confirmation du point de restauration contenant uniquement le code source ou la référence facultative de comparaison de l’étape 1 pour corriger la fonction concernée, puis relancez. Pour recréer une sortie existante après correction du code, conservez-la d’abord avec la sauvegarde dans un répertoire unique. Vous disposez maintenant d’un résultat complet et des preuves antérieures, plutôt que d’un fichier partiel qui semble seulement réussir.

Résumé

Vous avez implémenté la frontière des effets sur les fichiers après validation et transformation complètes des blocs. Les chemins de sortie existants ont été refusés sans remplacement, des données tardives incorrectes n’ont produit aucune exportation partielle, et les erreurs ordinaires de chemin, temps négatif et débordement sont restées récupérables. Vous avez conservé le travail antérieur dans des répertoires de sauvegarde uniques et vérifié de vrais sous-titres Unicode, multilignes et qui se chevauchent avec le vrai lecteur. Le défi combinera ces frontières apprises pour un autre court métrage et un décalage public différent.