Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 52 additions & 21 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ mlxcel-xla = { path = "src/lib/mlxcel-xla", optional = true }
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
serde_yaml = "0.9"
serde-saphyr = { version = "1.3", default-features = false, features = ["deserialize"] }

# Tensors & Models
safetensors = "0.7.0"
Expand Down
33 changes: 33 additions & 0 deletions TECHNICAL_REPORTS/2279-maintained-yaml-parser-20261012.en.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Technical report: PR #2279 — maintained YAML parser

**Date**: 2026-10-12
**Status**: Verified
**Risk level**: Medium

## Executive summary

The CLI and surgery configuration loaders now use deserialize-only serde-saphyr 1.3.0. This removes serde_yaml and unsafe-libyaml, the dependency chain that blocked CI under RUSTSEC-2024-0449.

## Problem statement

Both configuration paths depended on the unmaintained parser. An advisory suppression would leave the unsupported dependency in the runtime graph. The replacement must preserve document-order language priorities, schema validation, and operation-specific numeric errors.

## Technical decisions

Both loaders retain ordinary YAML merge keys and YAML 1.2 boolean interpretation. Duplicate mapping keys remain errors; a supported language key's duplicate failure maps to DuplicateLanguageCode. The streaming schema rejects unknown fields before they can be misclassified as language keys.

Serde's untagged and internally tagged buffering reads numbers through a typeless representation. serde-saphyr represents allowed non-finite typeless values as canonical strings, so the loaders opt into that representation and narrowly recover .nan, .inf, and -.inf. Surgery's existing validators still reject non-finite factors, coefficients, and ratios. Ordinary quoted surgery numbers remain invalid. Deliberate compatibility change: quoted canonical special values now use the same handling as unquoted values; parser diagnostic formatting also changes.

## Change summary

The lockfile removes both unsupported packages and adds only the replacement parser's dependency tree. Regression tests cover duplicate schema fields, ordered numeric entries, boolean spellings, non-finite operation validation, and all six shipped surgery YAML examples. Existing public signatures and model execution paths are unchanged.

## Validation

All 34 focused language-bias tests and 28 surgery configuration tests pass, including the added factor/alpha/ratio non-finite and quoted canonical regressions. cargo deny check passes advisories, bans, licenses, and sources. Formatting and diff checks pass. Repository contract checks pass, including 123 Python tooling tests and 49 WebUI fixtures. Final tests and CI Clippy, CPU-only link, and cargo-deny were verified on dd36abb9b321fa6eb6c9abaca07f5690ead72a17. The duplicate local CUDA Clippy rebuild was stopped after CI Clippy succeeded.

## References

- Issue #2278
- https://rustsec.org/advisories/RUSTSEC-2024-0449.html
- https://docs.rs/serde-saphyr/1.3.0/serde_saphyr/
33 changes: 33 additions & 0 deletions TECHNICAL_REPORTS/2279-maintained-yaml-parser-20261012.ko.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 기술 보고서: PR #2279 — 유지보수되는 YAML 파서

**작성일**: 2026-10-12
**상태**: 검증 완료
**위험도**: 중간

## 요약

CLI와 surgery 설정 로더가 역직렬화 기능만 활성화한 serde-saphyr 1.3.0을 사용합니다. RUSTSEC-2024-0449로 CI를 차단했던 serde_yaml과 unsafe-libyaml 의존성 경로를 제거합니다.

## 문제 정의

두 설정 경로가 유지보수되지 않는 파서에 의존했습니다. 보안 권고를 무시하면 지원되지 않는 의존성이 실행 경로에 남습니다. 교체 과정에서 문서 순서에 따른 언어 우선순위, 스키마 검증, 작업별 숫자 오류를 보존해야 합니다.

## 기술적 선택과 그 이유

두 로더는 YAML 병합 키를 일반 키로 처리하며 YAML 1.2 불리언 해석을 유지합니다. 중복 매핑 키는 계속 오류이며, 지원되는 언어 키의 중복 오류는 DuplicateLanguageCode로 변환합니다. 스트리밍 스키마 검증이 알 수 없는 필드를 먼저 거부하므로 이를 언어 키로 잘못 분류하지 않습니다.

Serde의 untagged 및 internally tagged 버퍼링은 숫자를 타입이 정해지지 않은 표현으로 읽습니다. serde-saphyr는 허용된 비유한 숫자를 정규화된 문자열로 표현하므로 로더에서 이 표현을 활성화하고 .nan, .inf, -.inf만 복원합니다. 기존 surgery 검증은 비유한 배율, 계수, 비율을 계속 거부합니다. 일반적인 따옴표 숫자는 계속 유효하지 않습니다. 명시적인 호환성 변경은 따옴표로 감싼 정규화된 특수 값도 따옴표 없는 값과 같은 처리를 받는 점과 파서 진단 출력 형식입니다.

## 변경 요약

잠금 파일에서 지원되지 않는 두 패키지를 제거하고 대체 파서의 의존성만 추가했습니다. 회귀 테스트는 중복 스키마 필드, 숫자 항목 순서, 불리언 표기, 비유한 숫자의 작업 검증 및 배포된 surgery YAML 예제 6개를 다룹니다. 공개 함수 시그니처와 모델 실행 경로는 그대로입니다.

## 검증

언어 바이어스 집중 테스트 34개와 surgery 설정 테스트 28개를 통과했습니다. 추가한 factor/alpha/ratio 비유한 숫자 및 따옴표 특수 값 회귀 테스트도 통과했습니다. cargo deny check의 advisories, bans, licenses, sources 검사와 포맷 및 패치 검사를 통과했습니다. Python 도구 테스트 123개와 WebUI 픽스처 49개를 포함한 저장소 계약 검사도 통과했습니다. 최종 테스트와 CI Clippy, CPU 전용 링크, cargo-deny는 dd36abb9b321fa6eb6c9abaca07f5690ead72a17에서 확인했습니다. 중복된 로컬 CUDA Clippy 재빌드는 CI Clippy 성공 후 중단했습니다.

## 참고 자료

- 이슈 #2278
- https://rustsec.org/advisories/RUSTSEC-2024-0449.html
- https://docs.rs/serde-saphyr/1.3.0/serde_saphyr/
68 changes: 44 additions & 24 deletions src/lang_bias.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,10 @@ pub struct LangBiasYamlConfig {
/// which made the resolve loop's duplicate check unreachable and let the YAML
/// path silently accept input the `--lang-bias` parser rejects. See issue #1267.
///
/// Collecting the entries through `MapAccess` into a `Vec` keeps both: the
/// author's order survives, and a repeated key arrives as a second entry that
/// the resolve loop can reject. The accepted YAML syntax is unchanged, `bias:`
/// is still a plain mapping.
/// Collecting entries through `MapAccess` into a `Vec` preserves their priority
/// order. The YAML parser rejects duplicate mapping keys before this visitor
/// receives them; `load_yaml_config` retains the specific language-code error.
/// The accepted YAML syntax is unchanged: `bias:` is still a plain mapping.
#[derive(Debug, Default)]
pub struct BiasEntries(Vec<(String, BiasValueStr)>);

Expand All @@ -223,7 +223,7 @@ impl BiasEntries {
&self.0
}

/// Number of entries, counting a repeated language code once per occurrence.
/// Number of entries in document order.
pub fn len(&self) -> usize {
self.0.len()
}
Expand Down Expand Up @@ -262,8 +262,8 @@ impl<'de> Deserialize<'de> for BiasEntries {
M: serde::de::MapAccess<'de>,
{
let mut entries = Vec::with_capacity(access.size_hint().unwrap_or(0));
// Deliberately a Vec push per entry rather than a map insert:
// repeated keys must reach the caller so it can reject them.
// Preserve parser-provided document order with a Vec push.
// The loader handles parser-level duplicate-key errors.
while let Some(entry) = access.next_entry::<String, BiasValueStr>()? {
entries.push(entry);
}
Expand Down Expand Up @@ -313,7 +313,12 @@ impl<'de> Deserialize<'de> for BiasValueStr {
let raw = RawBias::deserialize(deserializer)?;
let value = match raw {
RawBias::Float(f) => f as f32,
RawBias::Str(s) => parse_bias_f32(&s).map_err(serde::de::Error::custom)?,
RawBias::Str(s) => match s.as_str() {
".nan" => f32::NAN,
".inf" => f32::INFINITY,
"-.inf" => f32::NEG_INFINITY,
_ => parse_bias_f32(&s).map_err(serde::de::Error::custom)?,
},
};
Ok(BiasValueStr(value))
}
Expand Down Expand Up @@ -355,9 +360,25 @@ pub fn load_yaml_config(path: &PathBuf) -> Result<LangBiasYamlConfig, CliError>
source: e,
})?;

serde_yaml::from_str(&content).map_err(|e| CliError::ConfigParseError {
path: path.display().to_string(),
message: e.to_string(),
let options = serde_saphyr::options! {
merge_keys: serde_saphyr::MergeKeyPolicy::AsOrdinary,
strict_booleans: true,
reject_non_finite_typeless_float: false,
};
serde_saphyr::from_str_with_options(&content, options).map_err(|e| {
// Only bias mapping keys can be valid language codes in this schema.
// Keep the CLI diagnostic while retaining strict parser duplicate checks.
if let serde_saphyr::Error::DuplicateMappingKey {
key: Some(code), ..
} = e.without_snippet()
&& LanguageCode::from_str(code).is_ok()
{
return CliError::DuplicateLanguageCode { code: code.clone() };
}
CliError::ConfigParseError {
path: path.display().to_string(),
message: e.to_string(),
}
})
}

Expand Down Expand Up @@ -495,10 +516,8 @@ impl LangBiasCliArgs {
}

if let Some(yaml_bias) = yaml.bias {
// `BiasEntries` yields the `bias:` block in document order and
// keeps repeated keys, so `ordered` ends up in the priority
// order the author wrote and the duplicate check below actually
// fires. This mirrors `parse_lang_bias_entries` entry for entry.
// Preserve the author's priority order. The YAML loader rejects
// duplicates; retain this defensive check for other deserializers.
let mut ordered = Vec::new();
let mut seen: HashMap<String, ()> = HashMap::new();
for (code_str, BiasValueStr(bias)) in yaml_bias {
Expand Down Expand Up @@ -692,7 +711,7 @@ exceptions:
include_numeric: false
include_punctuation: true
"#;
let config: LangBiasYamlConfig = serde_yaml::from_str(yaml_str).unwrap();
let config: LangBiasYamlConfig = serde_saphyr::from_str(yaml_str).unwrap();
assert!(matches!(config.policy, Some(PolicyStr::Conservative)));
let bias = config.bias.unwrap();
// Assert the order, not just membership. Asserting membership alone is
Expand All @@ -714,7 +733,7 @@ exceptions:
let yaml_str = r#"
policy: strict
"#;
let config: LangBiasYamlConfig = serde_yaml::from_str(yaml_str).unwrap();
let config: LangBiasYamlConfig = serde_saphyr::from_str(yaml_str).unwrap();
assert!(config.bias.is_none());
}

Expand All @@ -724,7 +743,7 @@ policy: strict
policy: conservative
unknown_field: value
"#;
let result: Result<LangBiasYamlConfig, _> = serde_yaml::from_str(yaml_str);
let result: Result<LangBiasYamlConfig, _> = serde_saphyr::from_str(yaml_str);
assert!(
result.is_err(),
"unknown top-level key should produce a parse error"
Expand Down Expand Up @@ -904,11 +923,8 @@ unknown_field: value

#[test]
fn yaml_duplicate_language_code_is_rejected() {
// serde_yaml deserializes a repeated key into a typed `HashMap` without
// any diagnostic (last occurrence wins), which is why the duplicate
// check in `resolve` used to be unreachable. The ordered representation
// delivers both occurrences, so the YAML path now rejects what the
// `--lang-bias` path already rejected.
// The parser rejects duplicates before collecting bias entries. The
// loader maps that failure to the same specific error as the CLI path.
let (_tmpfile, path) = write_temp_yaml("bias:\n ja: -1.0\n zh: -2.0\n ja: -3.0\n");

let args = LangBiasCliArgs {
Expand Down Expand Up @@ -945,7 +961,7 @@ unknown_field: value
// The accepted syntax is unchanged: `bias:` is a mapping, and a list
// form is still a parse error rather than a silently different config.
let result: Result<LangBiasYamlConfig, _> =
serde_yaml::from_str("bias:\n - ja: -inf\n - zh: -10.0\n");
serde_saphyr::from_str("bias:\n - ja: -inf\n - zh: -10.0\n");
assert!(
result.is_err(),
"a sequence-shaped bias: block should still be a parse error"
Expand Down Expand Up @@ -1164,3 +1180,7 @@ unknown_field: value
assert_eq!(parse_bias_f32("-5.0").unwrap(), -5.0_f32);
}
}

#[cfg(test)]
#[path = "lang_bias_yaml_compat_tests.rs"]
mod yaml_compat_tests;
Loading