잘못된 이동을 거부하고 자막 보존하기

RustBeginner
지금 연습하기

소개

올바른 자막 변환은 처음 맞는 예제 뒤의 자막까지 포함해 파일 전체를 보호해야 합니다. 뒤쪽의 잘못된 블록 때문에 부분 결과가 남아서는 안 되며, 새 실행이 성공한 것처럼 보여도 기존 출력을 덮어쓰면 안 됩니다.

전체 블록 변환을 감싸는 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/헤더 메타데이터, 블록 ID/설정, NOTE/STYLE/REGION, 마크업/엔터티, 본문의 <, >, &, 탭 외의 제어 문자는 이 과정에서 지원하지 않으며 조용히 버리는 대신 거부합니다.

같은 실제 원본을 확인하고 미완성 내보내기를 의도적으로 시도합니다.

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

두 번째 명령은 transform is unfinished와 함께 0이 아닌 상태를 반환해야 하며 파일을 만들면 안 됩니다. 원래 자막은 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는 문자 크기로 잘라내지 않고 String의 기존 UTF-8 바이트를 빌립니다. shifted.len()은 블록 수이며 쓰기 성공 후에만 반환합니다.

import를 포함한 실제 시작 코드를 읽으세요.

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의 프로젝트 디렉터리에서 계속합니다. 다음 실패는 의도한 것이며 0이 아닌 상태를 반환해야 합니다. 성공 메시지를 출력하거나 요청한 새 출력을 만들면 안 됩니다.

먼저 성공한 명령을 기존 결과에 대해 다시 실행합니다.

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의 끝이 시작보다 빠릅니다. 올바른 앞부분만으로는 사용할 수 있는 내보내기가 아닙니다. 다음 명령은 0이 아닌 상태를 반환하고 실제 원본, 블록 2, 시간 줄을 알려야 합니다.

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

지정 경로를 나열하는 ls로 출력이 생기지 않았는지 확인합니다. 결과가 없으므로 이 명령도 0이 아닌 상태가 예상됩니다.

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 시작을 0 아래로 옮깁니다. 다음 명령은 값을 큰 양수로 돌아가게 하거나 잘라 맞추지 않고 이 이동을 거부해야 합니다.

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, 여러 줄, 중첩 자막을 실제 플레이어로 확인했습니다. 챌린지에서는 다른 짧은 영상과 다른 공개 오프셋을 위해 이 기법들을 조합합니다.