Введение
Правильное преобразование субтитров должно защищать весь файл, включая блоки после первого правильного примера. Неправильный блок дальше по файлу не должен оставлять частичный результат, а существующий выход нельзя перезаписывать только потому, что новый запуск выглядит успешным.
Вы реализуете на 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, минимум двумя цифрами часов, двумя цифрами минут/секунд в 00–59 и тремя цифрами миллисекунд. Полное время должно помещаться в неотрицательные миллисекунды i64. Начала не убывают; одинаковые начала и перекрытия допустимы; конец больше начала. В блоке есть одна или несколько непустых логических строк содержимого. Unicode, табуляции, несколько строк, начальные/конечные пробелы сохраняются, включая полностью пробельное содержимое. Действительно пустая строка завершает блок; отдельное продолжение текста после неё отклоняется. Вход допускает LF/CRLF, необязательный последний перевод строки и дополнительные пустые разделители. Подготовленный выход использует LF, две цифры часов для 0–99 часов (выше — только нужные дополнительные цифры), пустую строку после заголовка и после каждого блока, включая последний. Он нормализует запись временных меток и число разделителей, сохраняя логический текст содержимого. Отдельные возвраты каретки, BOM/метаданные заголовка, идентификаторы/настройки блоков, NOTE/STYLE/REGION, разметка/сущности, символы содержимого <, > или & и управляющие символы кроме табуляции не поддерживаются этим курсом и отклоняются, а не удаляются молча.
Посмотрите тот же реальный источник, затем намеренно попробуйте незавершённый экспорт:
cargo run --quiet -- inspect data/original.vtt
cargo run --quiet -- shift data/original.vtt output/aligned.vtt -2000
Вторая команда должна вернуть ненулевой статус с transform is unfinished и не создавать файл. Исходный субтитр начинается на 4000–7000 ms, зелёная сцена смонтированного фильма — на 2000 ms. Требуемый сдвиг всё ещё -2000 для каждой границы каждого блока.
Прежде чем читать объяснение, запишите свой безопасный порядок этих намеренно перемешанных действий: создать новую цель, сформировать текст, разобрать все блоки, записать байты, прочитать названный вход, сдвинуть каждый блок. Прочитайте пример с неправильным поздним блоком и предскажите, должна ли существовать цель, если второй блок не пройдёт проверку:
cat data/bad-late.vtt
Теперь сравните свой порядок с границей побочного эффекта. Граница файловых операций должна соблюдать такой порядок. Сначала прочитайте фактически названный вход как текст UTF-8. Разберите все блоки и отклоните неподдерживаемое или неправильное содержимое с контекстом блока. Затем сдвиньте всю коллекцию с проверкой арифметики, отклоняя отрицательные значения времени или переполнение. Сформируйте полный результат в памяти. Только после успеха всех этих действий создайте новый выход и запишите подготовленные байты.
Создание выхода до полного разбора, сдвига и форматирования могло бы оставить пустой или частичный файл после позднего сбоя. Запись с заменой могла бы уничтожить полезный прежний результат. OpenOptions задаёт способ открытия файла. .write(true).create_new(true) запрашивает новый записываемый файл; если путь уже занят любым объектом, включая символическую ссылку с несуществующей целью, открытие завершается ошибкой без усечения содержимого. Это атомарное решение о создании только нового файла устраняет гонку между отдельной проверкой существования и созданием. Оно не гарантирует общую транзакцию хранения при постороннем сбое ввода-вывода после создания.
Path — заимствованный путь файловой системы. fs::read_to_string возвращает полный UTF-8 String или ошибку ввода-вывода. map_err добавляет фактический путь сбоя, сохраняя сообщение; ? немедленно возвращает ошибку. Предоставленные parse, shift_all и render работают с полными данными в памяти. Write — стандартный трейт, предоставляющий write_all, который пытается записать каждый байт подготовленного буфера. Импорт делает метод доступным; реализовывать трейт не нужно. as_bytes заимствует существующие UTF-8-байты String без выделения срезов размером с символ. shifted.len() — число блоков, возвращаемое только после успешной записи.
Прочитайте настоящий начальный код, включая импорты:
cat src/export.rs
Сейчас импортирован только Path. Не предполагайте, что остальные имена доступны. Добавьте use std::fs::{self, OpenOptions}; для модуля файловой системы и построителя, use std::io::Write; для трейта, делающего write_all доступным, и 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()))?. Само форматирование возвращает String, а не Result. Затем настройте изменяемый выходной файл через 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 заменяет шесть символов 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
Восстановитесь с известным допустимым сдвигом и ещё одним новым именем файла:
cargo run --quiet -- shift data/original.vtt output/after-offset-errors.vtt -2000
После всех этих ошибок исходные субтитры и фильм по-прежнему доступны. Крайние случаи i64 проверяют арифметику, а не точное до миллисекунды воспроизведение браузером при огромных значениях времени; наблюдения в браузере относятся к этому короткому фильму.
Посмотрите другой допустимый источник с окончаниями строк CRLF, перекрывающимися блоками одинакового начала, полностью пробельным содержимым и Unicode-текстом:
cargo run --quiet -- inspect data/whitespace.vtt
В нём есть строка только из пробелов и одной табуляции. Она не пустая и потому относится к своему блоку. Действительно пустая строка, напротив, разделяет блоки; пустое содержимое или отдельное продолжение без времени не поддерживается. Подготовленное чтение сохраняет это различие и никогда молча не фильтрует блок. Экспортируйте допустимый случай через границу минуты:
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 и найдите зелёную сцену с полным двухстрочным Unicode-субтитром. На 4.5 должны быть видны оба перекрывающихся блока; на 60.5 янтарная сцена должна показать блок на границе минуты. Нажмите Play, чтобы проверить настоящее воспроизведение, затем поставьте на паузу. Загруженное имя файла указывает на фактический результат. Кнопки скачивания сохраняют загруженные файлы без их воссоздания.
Можно также выбрать output/whitespace-shifted.vtt и проверить 60.5. Загружаются оба блока с одинаковым началом; полностью пробельный субтитр может визуально выглядеть пустым. Сохранённый файл и текст загруженного блока содержат начальные/конечные пробелы и вторую строку другого блока, хотя вёрстка браузера не обязана отображать каждый пробел или табуляцию различимой шириной. Точный логический текст проверяют по файлу, а не по видимым промежуткам. Порядок активных блоков с одинаковым началом в браузере может отличаться; порядок блоков выходного файла сохраняется.
Вернитесь в Terminal и ещё раз изучите основной принимаемый результат:
cargo run --quiet -- inspect output/aligned.vtt
Если в команде опечатка или неправильный путь, исправляйте команду, а не защищённые данные. Если неверен Rust-код, используйте подтверждение контрольной копии только исходника или необязательный образец для сравнения из шага 1, чтобы исправить затронутую функцию, затем повторите запуск. При воссоздании существующего результата после исправления кода сначала сохраните его в уникальном резервном каталоге. Теперь у вас полный результат и прежние свидетельства, а не частичный файл, только выглядящий успешным.
Резюме
Вы реализовали границу файлового побочного эффекта после полной проверки и преобразования блоков. Существующие выходные пути отклонялись без замены, поздние неправильные данные не давали частичного экспорта, а обычные ошибки пути, отрицательного времени и переполнения оставались восстанавливаемыми. Вы сохранили прежнюю работу в уникальных резервных каталогах и проверили реальные Unicode-, многострочные и перекрывающиеся субтитры настоящим проигрывателем. Задание объединит эти изученные границы для другого короткого фильма с иным публичным сдвигом.



