Skip to content

Commit dc5ed85

Browse files
committed
fix(muxer): 使用 BCP 47 语言标签并禁用 IETF 规范化
mkvmerge 没有独立的 --language-ietf 输入选项,因此字幕轨道原先的 "--language-ietf 0:<ietf_language>" 写法无效。改为通过 --language 直接 传入 IETF BCP 47 标签(mkvmerge 会自动写入 LanguageIETF 与兼容的 legacy Language 元素)。同时在命令开头加入全局参数 --normalize-language-ietf off, 防止 mkvmerge 重写我们显式设置的语言标签。 fix(snapshot): 信任由已校验归档预览出的字幕字体路径 归档预览会在字体文件实际存在之前,把未来的提取路径写入计划,因此这些 路径不会出现在 snapshot.files 中。validate_plan_snapshot 现在会从 digest 已校验的字体归档重新计算出预览出的字体路径集合,并据此放行; 在 subset 模式下这一点尤其重要,因为规划期的回退逻辑可能保留全部预览 字体。真正不在归档预览中、也无法被追踪的字体路径仍会被拒绝(Untrusted attachment path),避免放宽校验带来的安全隐患。 test: 补充 muxer 与 snapshot 相关用例 - test_muxer_extended: 新增用例断言 --language-ietf 不再出现,且 --language 使用 BCP 47 标签;导入 SubtitleTrackPlan。 - test_snapshot: 新增两个 subset 模式用例,分别验证由已追踪归档预览出的 字体路径被接受,以及未在归档预览中的未信任字体路径被拒绝。
1 parent 089231b commit dc5ed85

4 files changed

Lines changed: 123 additions & 4 deletions

File tree

plexmuxy/muxer.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,9 @@ def runtime_failure(
129129

130130
def build_mkvmerge_command(plan: MuxPlan, output_path: Path, mkvmerge_path: str) -> list[str]:
131131
command = [mkvmerge_path, "--output", str(output_path)]
132+
# Keep mkvmerge from rewriting our explicit IETF language tags.
133+
command.append("--normalize-language-ietf")
134+
command.append("off")
132135
source_audio = [track for track in plan.source_tracks if track.type == "audio"]
133136
included_audio_ids = [track.id for track in source_audio if track.included]
134137
if source_audio and not included_audio_ids:
@@ -151,8 +154,10 @@ def build_mkvmerge_command(plan: MuxPlan, output_path: Path, mkvmerge_path: str)
151154
track = subtitle_by_path[path]
152155
command.extend([
153156
"--track-name", f"0:{track.track_name}",
154-
"--language", f"0:{track.mkv_language}",
155-
"--language-ietf", f"0:{track.ietf_language}",
157+
# mkvmerge's --language option accepts IETF BCP 47 tags and
158+
# writes both LanguageIETF and the compatible legacy Language
159+
# element. There is no separate --language-ietf input option.
160+
"--language", f"0:{track.ietf_language}",
156161
"--default-track-flag", f"0:{'yes' if track.default_track else 'no'}",
157162
"--forced-display-flag", f"0:{'yes' if track.forced_track else 'no'}",
158163
str(track.path),

plexmuxy/snapshot.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,44 @@ def validate_plan_snapshot(snapshot: MuxPlanSnapshot, config: AppConfig) -> None
161161
existed = {path.resolve() for path in snapshot.outputs_existing}
162162
from .planner import build_output_path
163163

164+
# Archive previews deliberately put future extraction paths in a plan before
165+
# those font files exist, so they cannot be present in ``snapshot.files``.
166+
# Recompute the paths from the digest-validated archives instead of broadly
167+
# trusting every untracked path below Fonts. This is especially important in
168+
# subset mode, where a planning-time fallback may retain all previewed fonts.
169+
previewed_archive_fonts: set[Path] = set()
170+
fonts_root = (snapshot.input_dir / "Fonts").resolve()
171+
archive_extensions = set(config.media.font_archive_extensions)
172+
untracked_attachments = {
173+
attachment.path.resolve()
174+
for plan in snapshot.plans
175+
for attachment in plan.attachments
176+
if attachment.path.resolve() not in tracked
177+
}
178+
archive_snapshots = [
179+
item
180+
for item in snapshot.files
181+
if item.path.suffix.casefold() in archive_extensions
182+
]
183+
if untracked_attachments and archive_snapshots:
184+
from .font import preview_font_archive
185+
186+
for archive_snapshot in archive_snapshots:
187+
try:
188+
previewed_archive_fonts.update(
189+
path.resolve()
190+
for path in preview_font_archive(
191+
archive_snapshot.path,
192+
fonts_root,
193+
config.media,
194+
config.font,
195+
)
196+
)
197+
except Exception as exc: # noqa: BLE001 - archive readers expose library-specific errors.
198+
raise StalePlanError(
199+
f"Planned font archive can no longer be inspected: {archive_snapshot.path}: {exc}"
200+
) from exc
201+
164202
for plan in snapshot.plans:
165203
output = plan.output_path.resolve()
166204
if plan.source_video.resolve() not in tracked:
@@ -184,13 +222,14 @@ def validate_plan_snapshot(snapshot: MuxPlanSnapshot, config: AppConfig) -> None
184222
expected_output = build_output_path(plan.source_video, snapshot.input_dir, config).resolve()
185223
if output != expected_output:
186224
raise StalePlanError(f"Plan output does not match the saved configuration: {output}")
187-
fonts_root = (snapshot.input_dir / "Fonts").resolve()
188225
for attachment in plan.attachments:
189226
attachment_path = attachment.path.resolve()
190227
if attachment_path in tracked:
191228
attachment_snapshot = tracked_snapshots[attachment_path]
192229
if snapshot.schema_version >= 2 and attachment_snapshot.sha256 is None:
193230
raise StalePlanError(f"Planned attachment has no digest: {attachment.path}")
231+
elif attachment_path in previewed_archive_fonts:
232+
continue
194233
elif font_mode == "subset" or fonts_root not in attachment_path.parents:
195234
raise StalePlanError(f"Untrusted attachment path in plan: {attachment.path}")
196235
intent = plan.font_subset_intent

tests/test_muxer_extended.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22
from types import SimpleNamespace
33

44
from plexmuxy.config import default_config
5-
from plexmuxy.models import AttachmentPlan, AudioTrackPlan, MuxPlan, SourceTrackInfo, VerificationResult
5+
from plexmuxy.models import (
6+
AttachmentPlan,
7+
AudioTrackPlan,
8+
MuxPlan,
9+
SourceTrackInfo,
10+
SubtitleTrackPlan,
11+
VerificationResult,
12+
)
613
from plexmuxy.muxer import build_mkvmerge_command, execute_mux_plan, inspect_source_tracks, verify_mux_output
714

815

@@ -79,6 +86,30 @@ def test_mkvmerge_command_uses_no_audio_only_for_source_input(tmp_path):
7986
assert command[-1] == str(external)
8087

8188

89+
def test_mkvmerge_command_sets_subtitle_bcp47_language_with_supported_option(tmp_path):
90+
subtitle = tmp_path / "Example.JPSC.ass"
91+
plan = MuxPlan(
92+
tmp_path / "source.mkv",
93+
tmp_path / "output.mkv",
94+
subtitle_tracks=[SubtitleTrackPlan(
95+
subtitle,
96+
"jp_sc Studio",
97+
"chi",
98+
"zh-Hans",
99+
True,
100+
False,
101+
"exact",
102+
)],
103+
)
104+
105+
command = build_mkvmerge_command(plan, plan.output_path, "mkvmerge")
106+
107+
assert "--language-ietf" not in command
108+
language_index = command.index("--language")
109+
assert command[language_index + 1] == "0:zh-Hans"
110+
assert command[-1] == str(subtitle)
111+
112+
82113
def test_execute_mux_plan_success_uses_verified_temp_then_replaces(monkeypatch, tmp_path):
83114
source = tmp_path / "source.mkv"
84115
source.write_bytes(b"source")

tests/test_snapshot.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import hashlib
22
import json
33
import os
4+
import zipfile
45

56
import pytest
67

78
from plexmuxy.config import config_to_dict, default_config, parse_config
89
from plexmuxy.errors import StalePlanError
910
from plexmuxy.models import (
11+
AttachmentPlan,
1012
FileSnapshot,
1113
FontFaceRef,
1214
FontSubsetGroupIntent,
@@ -219,6 +221,48 @@ def test_schema3_tracks_archive_not_future_extraction_path(tmp_path):
219221
validate_plan_snapshot(snapshot, config)
220222

221223

224+
def test_subset_snapshot_accepts_future_attachment_previewed_from_tracked_archive(tmp_path):
225+
video = tmp_path / "Example.mkv"
226+
archive = tmp_path / "Fonts.zip"
227+
future_font = tmp_path / "Fonts" / "Demo.otf"
228+
video.write_bytes(b"video")
229+
with zipfile.ZipFile(archive, "w") as bundle:
230+
bundle.writestr("Demo.otf", b"font payload")
231+
config = default_config()
232+
config.font.mode = "subset"
233+
plan = MuxPlan(
234+
video,
235+
tmp_path / "Example_Plex.mkv",
236+
attachments=[AttachmentPlan(future_font)],
237+
font_subset_intent=FontSubsetIntent(1, 1, (), ()),
238+
)
239+
snapshot = create_plan_snapshot(tmp_path, [plan], config, extra_inputs=[archive])
240+
241+
assert future_font.resolve() not in {item.path for item in snapshot.files}
242+
validate_plan_snapshot(snapshot, config)
243+
244+
245+
def test_subset_snapshot_rejects_untracked_font_not_in_archive_preview(tmp_path):
246+
video = tmp_path / "Example.mkv"
247+
archive = tmp_path / "Fonts.zip"
248+
untrusted_font = tmp_path / "Fonts" / "Injected.otf"
249+
video.write_bytes(b"video")
250+
with zipfile.ZipFile(archive, "w") as bundle:
251+
bundle.writestr("Demo.otf", b"font payload")
252+
config = default_config()
253+
config.font.mode = "subset"
254+
plan = MuxPlan(
255+
video,
256+
tmp_path / "Example_Plex.mkv",
257+
attachments=[AttachmentPlan(untrusted_font)],
258+
font_subset_intent=FontSubsetIntent(1, 1, (), ()),
259+
)
260+
snapshot = create_plan_snapshot(tmp_path, [plan], config, extra_inputs=[archive])
261+
262+
with pytest.raises(StalePlanError, match="Untrusted attachment path"):
263+
validate_plan_snapshot(snapshot, config)
264+
265+
222266
def test_schema1_v2_config_remains_valid_outside_subset_mode(tmp_path):
223267
video = tmp_path / "Example.mkv"
224268
video.write_bytes(b"video")

0 commit comments

Comments
 (0)