use std::io::Read;
#[derive(Debug, Default)]
pub struct JarLabels {
pub audio: Vec<TrackLabel>,
pub subtitle: Vec<TrackLabel>,
pub playlists: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct TrackLabel {
pub language: String,
pub hint: String,
pub variant: String,
pub description: String,
pub raw: String,
}
impl TrackLabel {
fn parse(s: &str) -> Option<Self> {
let clean = s.trim_end_matches('_');
let parts: Vec<&str> = clean.splitn(3, '_').collect();
if parts.len() < 2 {
return None;
}
let language = parts[0].to_string();
if language.len() < 2 || language.len() > 3 || !language.chars().all(|c| c.is_ascii_lowercase()) {
return None;
}
let hint = parts[1].to_string();
let variant = if parts.len() > 2 { parts[2].to_string() } else { String::new() };
let description = match hint.as_str() {
"MLP" => "TrueHD".to_string(),
"AC3" => {
if variant.is_empty() { "compatibility".to_string() }
else { variant.clone() }
}
"DTS" => "DTS".to_string(),
"LPCM" => "LPCM".to_string(),
"ADES" => {
if variant.is_empty() { "Descriptive Audio".to_string() }
else { format!("Descriptive Audio ({})", variant) }
}
h if h.starts_with("AudioStream") => String::new(), h if h.starts_with("PGStream") => String::new(), _ => String::new(),
};
Some(TrackLabel {
language,
hint,
variant,
description,
raw: s.to_string(),
})
}
fn is_audio(&self) -> bool {
matches!(self.hint.as_str(), "MLP" | "AC3" | "DTS" | "LPCM" | "ADES")
|| self.hint.starts_with("AudioStream")
}
fn is_subtitle(&self) -> bool {
self.hint.starts_with("PGStream")
}
}
pub fn extract_labels(jar_data: &[u8]) -> Option<JarLabels> {
let cursor = std::io::Cursor::new(jar_data);
let mut archive = zip::ZipArchive::new(cursor).ok()?;
let mut all_audio = Vec::new();
let mut all_subtitle = Vec::new();
let mut all_playlists = Vec::new();
for i in 0..archive.len() {
let mut file = archive.by_index(i).ok()?;
if !file.name().ends_with(".class") {
continue;
}
let mut data = Vec::new();
file.read_to_end(&mut data).ok()?;
let strings = extract_class_strings(&data);
for s in &strings {
if let Some(label) = TrackLabel::parse(s) {
if label.is_audio() && !all_audio.iter().any(|a: &TrackLabel| a.raw == label.raw) {
all_audio.push(label);
} else if label.is_subtitle() && !all_subtitle.iter().any(|a: &TrackLabel| a.raw == label.raw) {
all_subtitle.push(label);
}
}
if matches!(s.as_str(),
"MAIN_FEATURE" | "MAIN_FEATURE_INTRO" |
"FORCED_TRAILER" | "INTL_FORCED_TRAILER" |
"commentary_extras" | "extras"
) {
if !all_playlists.contains(s) {
all_playlists.push(s.clone());
}
}
}
}
if all_audio.is_empty() && all_subtitle.is_empty() {
return None;
}
Some(JarLabels {
audio: all_audio,
subtitle: all_subtitle,
playlists: all_playlists,
})
}
fn extract_class_strings(data: &[u8]) -> Vec<String> {
let mut strings = Vec::new();
if data.len() < 10 || &data[0..4] != &[0xCA, 0xFE, 0xBA, 0xBE] {
return strings;
}
let cp_count = ((data[8] as u16) << 8 | data[9] as u16) as usize;
let mut pos = 10;
let mut entry = 1; while entry < cp_count && pos < data.len() {
let tag = data[pos];
pos += 1;
match tag {
1 => {
if pos + 2 > data.len() { break; }
let len = ((data[pos] as usize) << 8) | data[pos + 1] as usize;
pos += 2;
if pos + len > data.len() { break; }
if let Ok(s) = std::str::from_utf8(&data[pos..pos + len]) {
if s.len() >= 5 && s.len() <= 100 && s.contains('_') {
strings.push(s.to_string());
}
}
pos += len;
}
3 | 4 => { pos += 4; }
5 | 6 => { pos += 8; entry += 1; }
7 | 8 | 16 => { pos += 2; }
9 | 10 | 11 | 12 | 18 => { pos += 4; }
15 => { pos += 3; }
_ => { break; }
}
entry += 1;
}
strings
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_audio_labels() {
let l = TrackLabel::parse("eng_MLP_").unwrap();
assert_eq!(l.language, "eng");
assert_eq!(l.hint, "MLP");
assert_eq!(l.description, "TrueHD");
assert!(l.is_audio());
let l = TrackLabel::parse("eng_ADES_US_").unwrap();
assert_eq!(l.language, "eng");
assert_eq!(l.hint, "ADES");
assert_eq!(l.variant, "US");
assert_eq!(l.description, "Descriptive Audio (US)");
assert!(l.is_audio());
let l = TrackLabel::parse("fra_AudioStream3").unwrap();
assert_eq!(l.language, "fra");
assert!(l.is_audio());
}
#[test]
fn test_parse_subtitle_labels() {
let l = TrackLabel::parse("dan_PGStream4").unwrap();
assert_eq!(l.language, "dan");
assert!(l.is_subtitle());
}
#[test]
fn test_reject_non_labels() {
assert!(TrackLabel::parse("substring").is_none());
assert!(TrackLabel::parse("equals").is_none());
assert!(TrackLabel::parse("Code").is_none());
}
}