implement
This commit is contained in:
486
crates/mirakurun-core/src/arib.rs
Normal file
486
crates/mirakurun-core/src/arib.rs
Normal file
@@ -0,0 +1,486 @@
|
||||
// Copyright 2026 Mirakurun contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use encoding_rs::EUC_JP;
|
||||
|
||||
#[path = "arib_symbols.rs"]
|
||||
mod symbols;
|
||||
|
||||
const ESC: u8 = 0x1b;
|
||||
const MACRO: u8 = 0x95;
|
||||
const MACRO_END: u8 = 0x4f;
|
||||
const MAX_MACRO_DEPTH: usize = 8;
|
||||
const DRCS_PRIVATE_USE_BASE: u32 = 0x0f_0000;
|
||||
const DRCS_0_COUNT: u32 = 94 * 94;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum GraphicSet {
|
||||
Kanji,
|
||||
Alphanumeric,
|
||||
Hiragana,
|
||||
Katakana,
|
||||
JisX0201Katakana,
|
||||
Mosaic(u8),
|
||||
JisPlane1,
|
||||
JisPlane2,
|
||||
AdditionalSymbols,
|
||||
Drcs(u8),
|
||||
Macro,
|
||||
Unsupported(u8),
|
||||
}
|
||||
|
||||
impl GraphicSet {
|
||||
const fn width(self) -> usize {
|
||||
match self {
|
||||
Self::Kanji
|
||||
| Self::JisPlane1
|
||||
| Self::JisPlane2
|
||||
| Self::AdditionalSymbols
|
||||
| Self::Drcs(0) => 2,
|
||||
Self::Unsupported(width) => width as usize,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Decoder {
|
||||
sets: [GraphicSet; 4],
|
||||
left: usize,
|
||||
right: usize,
|
||||
single_shift: Option<usize>,
|
||||
macros: HashMap<u8, Vec<u8>>,
|
||||
output: String,
|
||||
}
|
||||
|
||||
impl Default for Decoder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sets: [
|
||||
GraphicSet::Kanji,
|
||||
GraphicSet::Alphanumeric,
|
||||
GraphicSet::Hiragana,
|
||||
GraphicSet::Katakana,
|
||||
],
|
||||
left: 0,
|
||||
right: 2,
|
||||
single_shift: None,
|
||||
macros: HashMap::new(),
|
||||
output: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Decoder {
|
||||
fn decode(mut self, input: &[u8]) -> String {
|
||||
self.decode_sequence(input, 0);
|
||||
self.output
|
||||
}
|
||||
|
||||
fn decode_sequence(&mut self, input: &[u8], depth: usize) {
|
||||
if depth > MAX_MACRO_DEPTH {
|
||||
return;
|
||||
}
|
||||
let mut offset = 0;
|
||||
while offset < input.len() {
|
||||
let byte = input[offset];
|
||||
match byte {
|
||||
0x0e => self.left = 1,
|
||||
0x0f => self.left = 0,
|
||||
0x19 => self.single_shift = Some(2),
|
||||
0x1d => self.single_shift = Some(3),
|
||||
ESC => {
|
||||
offset += self.consume_escape(&input[offset..]).saturating_sub(1);
|
||||
}
|
||||
0x0d => self.output.push('\n'),
|
||||
0x16 => offset = offset.saturating_add(1),
|
||||
0x1c => offset = offset.saturating_add(2),
|
||||
0x20 | 0xa0 => self.output.push(' '),
|
||||
MACRO => {
|
||||
offset += self
|
||||
.consume_macro_definition(&input[offset..], depth)
|
||||
.saturating_sub(1);
|
||||
}
|
||||
0x9b => {
|
||||
offset += consume_csi(&input[offset..]).saturating_sub(1);
|
||||
}
|
||||
0x90 => {
|
||||
offset += consume_col(&input[offset..]).saturating_sub(1);
|
||||
}
|
||||
0x91..=0x94 | 0x97..=0x98 => offset = offset.saturating_add(1),
|
||||
0x9d => {
|
||||
offset += consume_time(&input[offset..]).saturating_sub(1);
|
||||
}
|
||||
0x21..=0x7e => {
|
||||
let set = self.single_shift.take().unwrap_or(self.left);
|
||||
let consumed =
|
||||
self.decode_graphic(self.sets[set], &input[offset..], false, depth);
|
||||
offset += consumed.saturating_sub(1);
|
||||
}
|
||||
0xa1..=0xfe => {
|
||||
let consumed =
|
||||
self.decode_graphic(self.sets[self.right], &input[offset..], true, depth);
|
||||
offset += consumed.saturating_sub(1);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
offset += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_graphic(
|
||||
&mut self,
|
||||
set: GraphicSet,
|
||||
input: &[u8],
|
||||
high_bit: bool,
|
||||
depth: usize,
|
||||
) -> usize {
|
||||
let width = set.width();
|
||||
if input.len() < width {
|
||||
return input.len().max(1);
|
||||
}
|
||||
let first = normalize_graphic(input[0], high_bit);
|
||||
let second = (width == 2).then(|| normalize_graphic(input[1], high_bit));
|
||||
if !(0x21..=0x7e).contains(&first)
|
||||
|| second.is_some_and(|value| !(0x21..=0x7e).contains(&value))
|
||||
{
|
||||
return width;
|
||||
}
|
||||
|
||||
match set {
|
||||
GraphicSet::Kanji | GraphicSet::JisPlane1 => {
|
||||
append_euc_jp(
|
||||
&[first | 0x80, second.unwrap_or(0) | 0x80],
|
||||
&mut self.output,
|
||||
);
|
||||
}
|
||||
GraphicSet::JisPlane2 => {
|
||||
append_euc_jp(
|
||||
&[0x8f, first | 0x80, second.unwrap_or(0) | 0x80],
|
||||
&mut self.output,
|
||||
);
|
||||
}
|
||||
GraphicSet::Alphanumeric => self.output.push(char::from(first)),
|
||||
GraphicSet::Hiragana => append_euc_jp(&[0xa4, first | 0x80], &mut self.output),
|
||||
GraphicSet::Katakana => append_euc_jp(&[0xa5, first | 0x80], &mut self.output),
|
||||
GraphicSet::JisX0201Katakana => {
|
||||
let codepoint = 0xff61 + u32::from(first - 0x21);
|
||||
self.output
|
||||
.push(char::from_u32(codepoint).unwrap_or('\u{fffd}'));
|
||||
}
|
||||
GraphicSet::Mosaic(kind) => self.output.push(mosaic_character(kind, first)),
|
||||
GraphicSet::AdditionalSymbols => {
|
||||
self.output.push(
|
||||
symbols::additional_symbol(first, second.unwrap_or(0)).unwrap_or('\u{fffd}'),
|
||||
);
|
||||
}
|
||||
GraphicSet::Drcs(set_number) => {
|
||||
self.output
|
||||
.push(drcs_character(set_number, first, second).unwrap_or('\u{fffd}'));
|
||||
}
|
||||
GraphicSet::Macro => self.invoke_macro(first, depth),
|
||||
GraphicSet::Unsupported(_) => self.output.push('\u{fffd}'),
|
||||
}
|
||||
width
|
||||
}
|
||||
|
||||
fn invoke_macro(&mut self, code: u8, depth: usize) {
|
||||
if let Some(body) = self.macros.get(&code).cloned() {
|
||||
self.decode_sequence(&body, depth + 1);
|
||||
} else {
|
||||
self.apply_default_macro(code);
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_default_macro(&mut self, code: u8) {
|
||||
let Some(sets) = default_macro_sets(code) else {
|
||||
return;
|
||||
};
|
||||
self.sets = sets;
|
||||
self.left = 0;
|
||||
self.right = 2;
|
||||
self.single_shift = None;
|
||||
}
|
||||
|
||||
fn consume_macro_definition(&mut self, input: &[u8], depth: usize) -> usize {
|
||||
let Some(&mode) = input.get(1) else {
|
||||
return 1;
|
||||
};
|
||||
if !matches!(mode, 0x40 | 0x41) {
|
||||
return 2;
|
||||
}
|
||||
let Some(&code) = input.get(2) else {
|
||||
return 2;
|
||||
};
|
||||
let mut end = 3;
|
||||
while end + 1 < input.len() {
|
||||
if input[end] == MACRO && input[end + 1] == MACRO_END {
|
||||
let body = input[3..end].to_vec();
|
||||
self.macros.insert(code, body.clone());
|
||||
if mode == 0x41 {
|
||||
self.decode_sequence(&body, depth + 1);
|
||||
}
|
||||
return end + 2;
|
||||
}
|
||||
end += 1;
|
||||
}
|
||||
input.len()
|
||||
}
|
||||
|
||||
fn consume_escape(&mut self, input: &[u8]) -> usize {
|
||||
let Some(&second) = input.get(1) else {
|
||||
return 1;
|
||||
};
|
||||
match second {
|
||||
0x6e => self.left = 2,
|
||||
0x6f => self.left = 3,
|
||||
0x7c => self.right = 3,
|
||||
0x7d => self.right = 2,
|
||||
0x7e => self.right = 1,
|
||||
0x28..=0x2b => {
|
||||
let Some(&third) = input.get(2) else {
|
||||
return 2;
|
||||
};
|
||||
let index = usize::from(second - 0x28);
|
||||
if third == 0x20 {
|
||||
let Some(&final_byte) = input.get(3) else {
|
||||
return 3;
|
||||
};
|
||||
self.sets[index] = drcs_set(final_byte, 1);
|
||||
return 4;
|
||||
}
|
||||
self.sets[index] =
|
||||
one_byte_graphic_set(third).unwrap_or(GraphicSet::Unsupported(1));
|
||||
return 3;
|
||||
}
|
||||
0x24 => {
|
||||
let Some(&third) = input.get(2) else {
|
||||
return 2;
|
||||
};
|
||||
if matches!(third, 0x28..=0x2b) {
|
||||
let index = usize::from(third - 0x28);
|
||||
let Some(&fourth) = input.get(3) else {
|
||||
return 3;
|
||||
};
|
||||
if fourth == 0x20 {
|
||||
let Some(&final_byte) = input.get(4) else {
|
||||
return 4;
|
||||
};
|
||||
self.sets[index] = drcs_set(final_byte, 2);
|
||||
return 5;
|
||||
}
|
||||
if fourth == 0x28 {
|
||||
self.sets[index] = GraphicSet::Unsupported(1);
|
||||
return input.get(4).map_or(4, |_| 5);
|
||||
}
|
||||
self.sets[index] =
|
||||
two_byte_graphic_set(fourth).unwrap_or(GraphicSet::Unsupported(2));
|
||||
return 4;
|
||||
}
|
||||
self.sets[0] = two_byte_graphic_set(third).unwrap_or(GraphicSet::Unsupported(2));
|
||||
return 3;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
2
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn decode(input: &[u8]) -> String {
|
||||
Decoder::default().decode(input)
|
||||
}
|
||||
|
||||
const fn normalize_graphic(byte: u8, high_bit: bool) -> u8 {
|
||||
if high_bit { byte & 0x7f } else { byte }
|
||||
}
|
||||
|
||||
fn append_euc_jp(bytes: &[u8], output: &mut String) {
|
||||
let (decoded, had_errors) = EUC_JP.decode_without_bom_handling(bytes);
|
||||
if had_errors {
|
||||
output.push('\u{fffd}');
|
||||
} else {
|
||||
output.push_str(&decoded);
|
||||
}
|
||||
}
|
||||
|
||||
const fn one_byte_graphic_set(code: u8) -> Option<GraphicSet> {
|
||||
match code {
|
||||
0x30 | 0x37 => Some(GraphicSet::Hiragana),
|
||||
0x31 | 0x38 => Some(GraphicSet::Katakana),
|
||||
0x32..=0x35 => Some(GraphicSet::Mosaic(code - 0x32)),
|
||||
0x36 | 0x4a => Some(GraphicSet::Alphanumeric),
|
||||
0x49 => Some(GraphicSet::JisX0201Katakana),
|
||||
0x70 => Some(GraphicSet::Macro),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
const fn two_byte_graphic_set(code: u8) -> Option<GraphicSet> {
|
||||
match code {
|
||||
0x39 => Some(GraphicSet::JisPlane1),
|
||||
0x3a => Some(GraphicSet::JisPlane2),
|
||||
0x3b => Some(GraphicSet::AdditionalSymbols),
|
||||
0x42 => Some(GraphicSet::Kanji),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
const fn drcs_set(final_byte: u8, width: u8) -> GraphicSet {
|
||||
if final_byte >= 0x40 && final_byte <= 0x4f {
|
||||
GraphicSet::Drcs(final_byte - 0x40)
|
||||
} else {
|
||||
GraphicSet::Unsupported(width)
|
||||
}
|
||||
}
|
||||
|
||||
fn drcs_character(set: u8, first: u8, second: Option<u8>) -> Option<char> {
|
||||
let index = if set == 0 {
|
||||
u32::from(first.checked_sub(0x21)?) * 94 + u32::from(second?.checked_sub(0x21)?)
|
||||
} else {
|
||||
DRCS_0_COUNT + u32::from(set.checked_sub(1)?) * 94 + u32::from(first.checked_sub(0x21)?)
|
||||
};
|
||||
char::from_u32(DRCS_PRIVATE_USE_BASE + index)
|
||||
}
|
||||
|
||||
const fn mosaic_character(kind: u8, code: u8) -> char {
|
||||
const MOSAIC: [char; 4] = ['▦', '▥', '▧', '▨'];
|
||||
let index = ((kind as usize) + (code as usize & 0x03)) % MOSAIC.len();
|
||||
MOSAIC[index]
|
||||
}
|
||||
|
||||
const fn default_macro_sets(code: u8) -> Option<[GraphicSet; 4]> {
|
||||
let macro_set = GraphicSet::Macro;
|
||||
match code {
|
||||
0x60 => Some([
|
||||
GraphicSet::Kanji,
|
||||
GraphicSet::Alphanumeric,
|
||||
GraphicSet::Hiragana,
|
||||
macro_set,
|
||||
]),
|
||||
0x61 => Some([
|
||||
GraphicSet::Kanji,
|
||||
GraphicSet::Katakana,
|
||||
GraphicSet::Hiragana,
|
||||
macro_set,
|
||||
]),
|
||||
0x62 => Some([
|
||||
GraphicSet::Kanji,
|
||||
GraphicSet::Drcs(1),
|
||||
GraphicSet::Hiragana,
|
||||
macro_set,
|
||||
]),
|
||||
0x63 => Some([
|
||||
GraphicSet::Mosaic(0),
|
||||
GraphicSet::Mosaic(2),
|
||||
GraphicSet::Mosaic(3),
|
||||
macro_set,
|
||||
]),
|
||||
0x64 => Some([
|
||||
GraphicSet::Mosaic(0),
|
||||
GraphicSet::Mosaic(1),
|
||||
GraphicSet::Mosaic(3),
|
||||
macro_set,
|
||||
]),
|
||||
0x65 => Some([
|
||||
GraphicSet::Mosaic(0),
|
||||
GraphicSet::Drcs(1),
|
||||
GraphicSet::Mosaic(3),
|
||||
macro_set,
|
||||
]),
|
||||
0x66..=0x6a => {
|
||||
let first = 1 + (code - 0x66) * 3;
|
||||
Some([
|
||||
GraphicSet::Drcs(first),
|
||||
GraphicSet::Drcs(first + 1),
|
||||
GraphicSet::Drcs(first + 2),
|
||||
macro_set,
|
||||
])
|
||||
}
|
||||
0x6b..=0x6d => Some([
|
||||
GraphicSet::Kanji,
|
||||
GraphicSet::Drcs(2 + code - 0x6b),
|
||||
GraphicSet::Hiragana,
|
||||
macro_set,
|
||||
]),
|
||||
0x6e => Some([
|
||||
GraphicSet::Katakana,
|
||||
GraphicSet::Hiragana,
|
||||
GraphicSet::Alphanumeric,
|
||||
macro_set,
|
||||
]),
|
||||
0x6f => Some([
|
||||
GraphicSet::Alphanumeric,
|
||||
GraphicSet::Mosaic(0),
|
||||
GraphicSet::Drcs(1),
|
||||
macro_set,
|
||||
]),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_csi(input: &[u8]) -> usize {
|
||||
input
|
||||
.iter()
|
||||
.enumerate()
|
||||
.skip(1)
|
||||
.find_map(|(index, byte)| (0x40..=0x7e).contains(byte).then_some(index + 1))
|
||||
.unwrap_or(input.len())
|
||||
}
|
||||
|
||||
fn consume_col(input: &[u8]) -> usize {
|
||||
match input.get(1) {
|
||||
Some(0x20) => input.len().min(3),
|
||||
Some(_) => input.len().min(2),
|
||||
None => 1,
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_time(input: &[u8]) -> usize {
|
||||
match input.get(1) {
|
||||
Some(0x20 | 0x28) => input.len().min(3),
|
||||
Some(_) => consume_csi(input),
|
||||
None => 1,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{DRCS_PRIVATE_USE_BASE, decode};
|
||||
|
||||
#[test]
|
||||
fn decodes_common_character_sets() {
|
||||
assert_eq!(decode(&[0x0e, b'T', b'e', b's', b't']), "Test");
|
||||
assert_eq!(decode(&[0x19, 0x22]), "あ");
|
||||
assert_eq!(decode(&[0x1d, 0x22]), "ア");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_additional_symbols_and_kanji() {
|
||||
assert_eq!(decode(&[0x1b, 0x24, 0x3b, 0x7a, 0x30]), "🅿");
|
||||
assert_eq!(decode(&[0x1b, 0x24, 0x3b, 0x75, 0x21]), "㐂");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_drcs_identity_in_private_use_plane() {
|
||||
let expected = char::from_u32(DRCS_PRIVATE_USE_BASE).map(String::from);
|
||||
assert_eq!(
|
||||
decode(&[0x1b, 0x24, 0x28, 0x20, 0x40, 0x21, 0x21]),
|
||||
expected.unwrap_or_default()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn applies_default_and_dynamically_defined_macros() {
|
||||
assert_eq!(decode(&[0x1b, 0x2b, 0x70, 0x1d, 0x61, 0x0e, 0x22]), "ア");
|
||||
assert_eq!(
|
||||
decode(&[
|
||||
0x1b, 0x2b, 0x70, 0x95, 0x40, 0x21, 0x0e, 0x95, 0x4f, 0x1d, 0x21, b'A',
|
||||
]),
|
||||
"A"
|
||||
);
|
||||
}
|
||||
}
|
||||
548
crates/mirakurun-core/src/arib_symbols.rs
Normal file
548
crates/mirakurun-core/src/arib_symbols.rs
Normal file
@@ -0,0 +1,548 @@
|
||||
// Copyright 2026 Mirakurun contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
// ARIB STD-B24 additional Kanji and symbol mappings.
|
||||
#[allow(
|
||||
clippy::match_same_arms,
|
||||
clippy::too_many_lines,
|
||||
clippy::unreadable_literal
|
||||
)]
|
||||
pub(super) fn additional_symbol(first: u8, second: u8) -> Option<char> {
|
||||
let code = (u16::from(first) << 8) | u16::from(second);
|
||||
match code {
|
||||
0x7A21 => char::from_u32(0x000026CC),
|
||||
0x7A22 => char::from_u32(0x000026CD),
|
||||
0x7A23 => char::from_u32(0x00002757),
|
||||
0x7A24 => char::from_u32(0x000026CF),
|
||||
0x7A25 => char::from_u32(0x000026D0),
|
||||
0x7A26 => char::from_u32(0x000026D1),
|
||||
0x7A28 => char::from_u32(0x000026D2),
|
||||
0x7A29 => char::from_u32(0x000026D5),
|
||||
0x7A2A => char::from_u32(0x000026D3),
|
||||
0x7A2B => char::from_u32(0x000026D4),
|
||||
0x7A30 => char::from_u32(0x0001F17F),
|
||||
0x7A31 => char::from_u32(0x0001F18A),
|
||||
0x7A34 => char::from_u32(0x000026D6),
|
||||
0x7A35 => char::from_u32(0x000026D7),
|
||||
0x7A36 => char::from_u32(0x000026D8),
|
||||
0x7A37 => char::from_u32(0x000026D9),
|
||||
0x7A38 => char::from_u32(0x000026DA),
|
||||
0x7A39 => char::from_u32(0x000026DB),
|
||||
0x7A3A => char::from_u32(0x000026DC),
|
||||
0x7A3B => char::from_u32(0x000026DD),
|
||||
0x7A3C => char::from_u32(0x000026DE),
|
||||
0x7A3D => char::from_u32(0x000026DF),
|
||||
0x7A3E => char::from_u32(0x000026E0),
|
||||
0x7A3F => char::from_u32(0x000026E1),
|
||||
0x7A40 => char::from_u32(0x00002B55),
|
||||
0x7A41 => char::from_u32(0x00003248),
|
||||
0x7A42 => char::from_u32(0x00003249),
|
||||
0x7A43 => char::from_u32(0x0000324A),
|
||||
0x7A44 => char::from_u32(0x0000324B),
|
||||
0x7A45 => char::from_u32(0x0000324C),
|
||||
0x7A46 => char::from_u32(0x0000324D),
|
||||
0x7A47 => char::from_u32(0x0000324E),
|
||||
0x7A48 => char::from_u32(0x0000324F),
|
||||
0x7A4D => char::from_u32(0x00002491),
|
||||
0x7A4E => char::from_u32(0x00002492),
|
||||
0x7A4F => char::from_u32(0x00002493),
|
||||
0x7A50 => char::from_u32(0x0001F14A),
|
||||
0x7A51 => char::from_u32(0x0001F14C),
|
||||
0x7A52 => char::from_u32(0x0001F13F),
|
||||
0x7A53 => char::from_u32(0x0001F146),
|
||||
0x7A54 => char::from_u32(0x0001F14B),
|
||||
0x7A55 => char::from_u32(0x0001F210),
|
||||
0x7A56 => char::from_u32(0x0001F211),
|
||||
0x7A57 => char::from_u32(0x0001F212),
|
||||
0x7A58 => char::from_u32(0x0001F213),
|
||||
0x7A59 => char::from_u32(0x0001F142),
|
||||
0x7A5A => char::from_u32(0x0001F214),
|
||||
0x7A5B => char::from_u32(0x0001F215),
|
||||
0x7A5C => char::from_u32(0x0001F216),
|
||||
0x7A5D => char::from_u32(0x0001F14D),
|
||||
0x7A5E => char::from_u32(0x0001F131),
|
||||
0x7A5F => char::from_u32(0x0001F13D),
|
||||
0x7A60 => char::from_u32(0x00002B1B),
|
||||
0x7A61 => char::from_u32(0x00002B24),
|
||||
0x7A62 => char::from_u32(0x0001F217),
|
||||
0x7A63 => char::from_u32(0x0001F218),
|
||||
0x7A64 => char::from_u32(0x0001F219),
|
||||
0x7A65 => char::from_u32(0x0001F21A),
|
||||
0x7A66 => char::from_u32(0x0001F21B),
|
||||
0x7A67 => char::from_u32(0x000026BF),
|
||||
0x7A68 => char::from_u32(0x0001F21C),
|
||||
0x7A69 => char::from_u32(0x0001F21D),
|
||||
0x7A6A => char::from_u32(0x0001F21E),
|
||||
0x7A6B => char::from_u32(0x0001F21F),
|
||||
0x7A6C => char::from_u32(0x0001F220),
|
||||
0x7A6D => char::from_u32(0x0001F221),
|
||||
0x7A6E => char::from_u32(0x0001F222),
|
||||
0x7A6F => char::from_u32(0x0001F223),
|
||||
0x7A70 => char::from_u32(0x0001F224),
|
||||
0x7A71 => char::from_u32(0x0001F225),
|
||||
0x7A72 => char::from_u32(0x0001F14E),
|
||||
0x7A73 => char::from_u32(0x00003299),
|
||||
0x7A74 => char::from_u32(0x0001F200),
|
||||
0x7B21 => char::from_u32(0x000026E3),
|
||||
0x7B22 => char::from_u32(0x00002B56),
|
||||
0x7B23 => char::from_u32(0x00002B57),
|
||||
0x7B24 => char::from_u32(0x00002B58),
|
||||
0x7B25 => char::from_u32(0x00002B59),
|
||||
0x7B26 => char::from_u32(0x00002613),
|
||||
0x7B27 => char::from_u32(0x0000328B),
|
||||
0x7B28 => char::from_u32(0x00003012),
|
||||
0x7B29 => char::from_u32(0x000026E8),
|
||||
0x7B2A => char::from_u32(0x00003246),
|
||||
0x7B2B => char::from_u32(0x00003245),
|
||||
0x7B2C => char::from_u32(0x000026E9),
|
||||
0x7B2D => char::from_u32(0x00000FD6),
|
||||
0x7B2E => char::from_u32(0x000026EA),
|
||||
0x7B2F => char::from_u32(0x000026EB),
|
||||
0x7B30 => char::from_u32(0x000026EC),
|
||||
0x7B31 => char::from_u32(0x00002668),
|
||||
0x7B32 => char::from_u32(0x000026ED),
|
||||
0x7B33 => char::from_u32(0x000026EE),
|
||||
0x7B34 => char::from_u32(0x000026EF),
|
||||
0x7B35 => char::from_u32(0x00002693),
|
||||
0x7B36 => char::from_u32(0x00002708),
|
||||
0x7B37 => char::from_u32(0x000026F0),
|
||||
0x7B38 => char::from_u32(0x000026F1),
|
||||
0x7B39 => char::from_u32(0x000026F2),
|
||||
0x7B3A => char::from_u32(0x000026F3),
|
||||
0x7B3B => char::from_u32(0x000026F4),
|
||||
0x7B3C => char::from_u32(0x000026F5),
|
||||
0x7B3D => char::from_u32(0x0001F157),
|
||||
0x7B3E => char::from_u32(0x000024B9),
|
||||
0x7B3F => char::from_u32(0x000024C8),
|
||||
0x7B40 => char::from_u32(0x000026F6),
|
||||
0x7B41 => char::from_u32(0x0001F15F),
|
||||
0x7B42 => char::from_u32(0x0001F18B),
|
||||
0x7B43 => char::from_u32(0x0001F18D),
|
||||
0x7B44 => char::from_u32(0x0001F18C),
|
||||
0x7B45 => char::from_u32(0x0001F179),
|
||||
0x7B46 => char::from_u32(0x000026F7),
|
||||
0x7B47 => char::from_u32(0x000026F8),
|
||||
0x7B48 => char::from_u32(0x000026F9),
|
||||
0x7B49 => char::from_u32(0x000026FA),
|
||||
0x7B4A => char::from_u32(0x0001F17B),
|
||||
0x7B4B => char::from_u32(0x0000260E),
|
||||
0x7B4C => char::from_u32(0x000026FB),
|
||||
0x7B4D => char::from_u32(0x000026FC),
|
||||
0x7B4E => char::from_u32(0x000026FD),
|
||||
0x7B4F => char::from_u32(0x000026FE),
|
||||
0x7B50 => char::from_u32(0x0001F17C),
|
||||
0x7B51 => char::from_u32(0x000026FF),
|
||||
0x7C21 => char::from_u32(0x000027A1),
|
||||
0x7C22 => char::from_u32(0x00002B05),
|
||||
0x7C23 => char::from_u32(0x00002B06),
|
||||
0x7C24 => char::from_u32(0x00002B07),
|
||||
0x7C25 => char::from_u32(0x00002B2F),
|
||||
0x7C26 => char::from_u32(0x00002B2E),
|
||||
0x7C27 => char::from_u32(0x00005E74),
|
||||
0x7C28 => char::from_u32(0x00006708),
|
||||
0x7C29 => char::from_u32(0x000065E5),
|
||||
0x7C2A => char::from_u32(0x00005186),
|
||||
0x7C2B => char::from_u32(0x000033A1),
|
||||
0x7C2C => char::from_u32(0x000033A5),
|
||||
0x7C2D => char::from_u32(0x0000339D),
|
||||
0x7C2E => char::from_u32(0x000033A0),
|
||||
0x7C2F => char::from_u32(0x000033A4),
|
||||
0x7C30 => char::from_u32(0x0001F100),
|
||||
0x7C31 => char::from_u32(0x00002488),
|
||||
0x7C32 => char::from_u32(0x00002489),
|
||||
0x7C33 => char::from_u32(0x0000248A),
|
||||
0x7C34 => char::from_u32(0x0000248B),
|
||||
0x7C35 => char::from_u32(0x0000248C),
|
||||
0x7C36 => char::from_u32(0x0000248D),
|
||||
0x7C37 => char::from_u32(0x0000248E),
|
||||
0x7C38 => char::from_u32(0x0000248F),
|
||||
0x7C39 => char::from_u32(0x00002490),
|
||||
0x7C3A => char::from_u32(0x0000E290),
|
||||
0x7C3B => char::from_u32(0x0000E291),
|
||||
0x7C3C => char::from_u32(0x0000E292),
|
||||
0x7C3D => char::from_u32(0x0000E293),
|
||||
0x7C3E => char::from_u32(0x0000E294),
|
||||
0x7C3F => char::from_u32(0x0000E295),
|
||||
0x7C40 => char::from_u32(0x0001F101),
|
||||
0x7C41 => char::from_u32(0x0001F102),
|
||||
0x7C42 => char::from_u32(0x0001F103),
|
||||
0x7C43 => char::from_u32(0x0001F104),
|
||||
0x7C44 => char::from_u32(0x0001F105),
|
||||
0x7C45 => char::from_u32(0x0001F106),
|
||||
0x7C46 => char::from_u32(0x0001F107),
|
||||
0x7C47 => char::from_u32(0x0001F108),
|
||||
0x7C48 => char::from_u32(0x0001F109),
|
||||
0x7C49 => char::from_u32(0x0001F10A),
|
||||
0x7C4A => char::from_u32(0x00003233),
|
||||
0x7C4B => char::from_u32(0x00003236),
|
||||
0x7C4C => char::from_u32(0x00003232),
|
||||
0x7C4D => char::from_u32(0x00003231),
|
||||
0x7C4E => char::from_u32(0x00003239),
|
||||
0x7C4F => char::from_u32(0x00003244),
|
||||
0x7C50 => char::from_u32(0x000025B6),
|
||||
0x7C51 => char::from_u32(0x000025C0),
|
||||
0x7C52 => char::from_u32(0x00003016),
|
||||
0x7C53 => char::from_u32(0x00003017),
|
||||
0x7C54 => char::from_u32(0x000027D0),
|
||||
0x7C55 => char::from_u32(0x000000B2),
|
||||
0x7C56 => char::from_u32(0x000000B3),
|
||||
0x7C57 => char::from_u32(0x0001F12D),
|
||||
0x7C58 => char::from_u32(0x0000E2A5),
|
||||
0x7C59 => char::from_u32(0x0000E2A6),
|
||||
0x7C5A => char::from_u32(0x0000E2A7),
|
||||
0x7C5B => char::from_u32(0x0000E2A8),
|
||||
0x7C5C => char::from_u32(0x0000E2A9),
|
||||
0x7C5D => char::from_u32(0x0000E2AA),
|
||||
0x7C5E => char::from_u32(0x0000E2AB),
|
||||
0x7C5F => char::from_u32(0x0000E2AC),
|
||||
0x7C60 => char::from_u32(0x0000E2AD),
|
||||
0x7C61 => char::from_u32(0x0000E2AE),
|
||||
0x7C62 => char::from_u32(0x0000E2AF),
|
||||
0x7C63 => char::from_u32(0x0000E2B0),
|
||||
0x7C64 => char::from_u32(0x0000E2B1),
|
||||
0x7C65 => char::from_u32(0x0000E2B2),
|
||||
0x7C66 => char::from_u32(0x0000E2B3),
|
||||
0x7C67 => char::from_u32(0x0000E2B4),
|
||||
0x7C68 => char::from_u32(0x0000E2B5),
|
||||
0x7C69 => char::from_u32(0x0000E2B6),
|
||||
0x7C6A => char::from_u32(0x0000E2B7),
|
||||
0x7C6B => char::from_u32(0x0000E2B8),
|
||||
0x7C6C => char::from_u32(0x0000E2B9),
|
||||
0x7C6D => char::from_u32(0x0000E2BA),
|
||||
0x7C6E => char::from_u32(0x0000E2BB),
|
||||
0x7C6F => char::from_u32(0x0000E2BC),
|
||||
0x7C70 => char::from_u32(0x0000E2BD),
|
||||
0x7C71 => char::from_u32(0x0000E2BE),
|
||||
0x7C72 => char::from_u32(0x0000E2BF),
|
||||
0x7C73 => char::from_u32(0x0000E2C0),
|
||||
0x7C74 => char::from_u32(0x0000E2C1),
|
||||
0x7C75 => char::from_u32(0x0000E2C2),
|
||||
0x7C76 => char::from_u32(0x0001F12C),
|
||||
0x7C77 => char::from_u32(0x0001F12B),
|
||||
0x7C78 => char::from_u32(0x00003247),
|
||||
0x7C79 => char::from_u32(0x0001F190),
|
||||
0x7C7A => char::from_u32(0x0001F226),
|
||||
0x7C7B => char::from_u32(0x0000213B),
|
||||
0x7D21 => char::from_u32(0x0000322A),
|
||||
0x7D22 => char::from_u32(0x0000322B),
|
||||
0x7D23 => char::from_u32(0x0000322C),
|
||||
0x7D24 => char::from_u32(0x0000322D),
|
||||
0x7D25 => char::from_u32(0x0000322E),
|
||||
0x7D26 => char::from_u32(0x0000322F),
|
||||
0x7D27 => char::from_u32(0x00003230),
|
||||
0x7D28 => char::from_u32(0x00003237),
|
||||
0x7D29 => char::from_u32(0x0000337E),
|
||||
0x7D2A => char::from_u32(0x0000337D),
|
||||
0x7D2B => char::from_u32(0x0000337C),
|
||||
0x7D2C => char::from_u32(0x0000337B),
|
||||
0x7D2D => char::from_u32(0x00002116),
|
||||
0x7D2E => char::from_u32(0x00002121),
|
||||
0x7D2F => char::from_u32(0x00003036),
|
||||
0x7D30 => char::from_u32(0x000026BE),
|
||||
0x7D31 => char::from_u32(0x0001F240),
|
||||
0x7D32 => char::from_u32(0x0001F241),
|
||||
0x7D33 => char::from_u32(0x0001F242),
|
||||
0x7D34 => char::from_u32(0x0001F243),
|
||||
0x7D35 => char::from_u32(0x0001F244),
|
||||
0x7D36 => char::from_u32(0x0001F245),
|
||||
0x7D37 => char::from_u32(0x0001F246),
|
||||
0x7D38 => char::from_u32(0x0001F247),
|
||||
0x7D39 => char::from_u32(0x0001F248),
|
||||
0x7D3A => char::from_u32(0x0001F12A),
|
||||
0x7D3B => char::from_u32(0x0001F227),
|
||||
0x7D3C => char::from_u32(0x0001F228),
|
||||
0x7D3D => char::from_u32(0x0001F229),
|
||||
0x7D3E => char::from_u32(0x0001F214),
|
||||
0x7D3F => char::from_u32(0x0001F22A),
|
||||
0x7D40 => char::from_u32(0x0001F22B),
|
||||
0x7D41 => char::from_u32(0x0001F22C),
|
||||
0x7D42 => char::from_u32(0x0001F22D),
|
||||
0x7D43 => char::from_u32(0x0001F22E),
|
||||
0x7D44 => char::from_u32(0x0001F22F),
|
||||
0x7D45 => char::from_u32(0x0001F230),
|
||||
0x7D46 => char::from_u32(0x0001F231),
|
||||
0x7D47 => char::from_u32(0x00002113),
|
||||
0x7D48 => char::from_u32(0x0000338F),
|
||||
0x7D49 => char::from_u32(0x00003390),
|
||||
0x7D4A => char::from_u32(0x000033CA),
|
||||
0x7D4B => char::from_u32(0x0000339E),
|
||||
0x7D4C => char::from_u32(0x000033A2),
|
||||
0x7D4D => char::from_u32(0x00003371),
|
||||
0x7D50 => char::from_u32(0x000000BD),
|
||||
0x7D51 => char::from_u32(0x00002189),
|
||||
0x7D52 => char::from_u32(0x00002153),
|
||||
0x7D53 => char::from_u32(0x00002154),
|
||||
0x7D54 => char::from_u32(0x000000BC),
|
||||
0x7D55 => char::from_u32(0x000000BE),
|
||||
0x7D56 => char::from_u32(0x00002155),
|
||||
0x7D57 => char::from_u32(0x00002156),
|
||||
0x7D58 => char::from_u32(0x00002157),
|
||||
0x7D59 => char::from_u32(0x00002158),
|
||||
0x7D5A => char::from_u32(0x00002159),
|
||||
0x7D5B => char::from_u32(0x0000215A),
|
||||
0x7D5C => char::from_u32(0x00002150),
|
||||
0x7D5D => char::from_u32(0x0000215B),
|
||||
0x7D5E => char::from_u32(0x00002151),
|
||||
0x7D5F => char::from_u32(0x00002152),
|
||||
0x7D60 => char::from_u32(0x00002600),
|
||||
0x7D61 => char::from_u32(0x00002601),
|
||||
0x7D62 => char::from_u32(0x00002602),
|
||||
0x7D63 => char::from_u32(0x000026C4),
|
||||
0x7D64 => char::from_u32(0x00002616),
|
||||
0x7D65 => char::from_u32(0x00002617),
|
||||
0x7D66 => char::from_u32(0x000026C9),
|
||||
0x7D67 => char::from_u32(0x000026CA),
|
||||
0x7D68 => char::from_u32(0x00002666),
|
||||
0x7D69 => char::from_u32(0x00002665),
|
||||
0x7D6A => char::from_u32(0x00002663),
|
||||
0x7D6B => char::from_u32(0x00002660),
|
||||
0x7D6C => char::from_u32(0x000026CB),
|
||||
0x7D6D => char::from_u32(0x00002A00),
|
||||
0x7D6E => char::from_u32(0x0000203C),
|
||||
0x7D6F => char::from_u32(0x00002049),
|
||||
0x7D70 => char::from_u32(0x000026C5),
|
||||
0x7D71 => char::from_u32(0x00002614),
|
||||
0x7D72 => char::from_u32(0x000026C6),
|
||||
0x7D73 => char::from_u32(0x00002603),
|
||||
0x7D74 => char::from_u32(0x000026C7),
|
||||
0x7D75 => char::from_u32(0x000026A1),
|
||||
0x7D76 => char::from_u32(0x000026C8),
|
||||
0x7D78 => char::from_u32(0x0000269E),
|
||||
0x7D79 => char::from_u32(0x0000269F),
|
||||
0x7D7A => char::from_u32(0x0000266C),
|
||||
0x7D7B => char::from_u32(0x0000260E),
|
||||
0x7E21 => char::from_u32(0x00002160),
|
||||
0x7E22 => char::from_u32(0x00002161),
|
||||
0x7E23 => char::from_u32(0x00002162),
|
||||
0x7E24 => char::from_u32(0x00002163),
|
||||
0x7E25 => char::from_u32(0x00002164),
|
||||
0x7E26 => char::from_u32(0x00002165),
|
||||
0x7E27 => char::from_u32(0x00002166),
|
||||
0x7E28 => char::from_u32(0x00002167),
|
||||
0x7E29 => char::from_u32(0x00002168),
|
||||
0x7E2A => char::from_u32(0x00002169),
|
||||
0x7E2B => char::from_u32(0x0000216A),
|
||||
0x7E2C => char::from_u32(0x0000216B),
|
||||
0x7E2D => char::from_u32(0x00002470),
|
||||
0x7E2E => char::from_u32(0x00002471),
|
||||
0x7E2F => char::from_u32(0x00002472),
|
||||
0x7E30 => char::from_u32(0x00002473),
|
||||
0x7E31 => char::from_u32(0x00002474),
|
||||
0x7E32 => char::from_u32(0x00002475),
|
||||
0x7E33 => char::from_u32(0x00002476),
|
||||
0x7E34 => char::from_u32(0x00002477),
|
||||
0x7E35 => char::from_u32(0x00002478),
|
||||
0x7E36 => char::from_u32(0x00002479),
|
||||
0x7E37 => char::from_u32(0x0000247A),
|
||||
0x7E38 => char::from_u32(0x0000247B),
|
||||
0x7E39 => char::from_u32(0x0000247C),
|
||||
0x7E3A => char::from_u32(0x0000247D),
|
||||
0x7E3B => char::from_u32(0x0000247E),
|
||||
0x7E3C => char::from_u32(0x0000247F),
|
||||
0x7E3D => char::from_u32(0x00003251),
|
||||
0x7E3E => char::from_u32(0x00003252),
|
||||
0x7E3F => char::from_u32(0x00003253),
|
||||
0x7E40 => char::from_u32(0x00003254),
|
||||
0x7E41 => char::from_u32(0x0001F110),
|
||||
0x7E42 => char::from_u32(0x0001F111),
|
||||
0x7E43 => char::from_u32(0x0001F112),
|
||||
0x7E44 => char::from_u32(0x0001F113),
|
||||
0x7E45 => char::from_u32(0x0001F114),
|
||||
0x7E46 => char::from_u32(0x0001F115),
|
||||
0x7E47 => char::from_u32(0x0001F116),
|
||||
0x7E48 => char::from_u32(0x0001F117),
|
||||
0x7E49 => char::from_u32(0x0001F118),
|
||||
0x7E4A => char::from_u32(0x0001F119),
|
||||
0x7E4B => char::from_u32(0x0001F11A),
|
||||
0x7E4C => char::from_u32(0x0001F11B),
|
||||
0x7E4D => char::from_u32(0x0001F11C),
|
||||
0x7E4E => char::from_u32(0x0001F11D),
|
||||
0x7E4F => char::from_u32(0x0001F11E),
|
||||
0x7E50 => char::from_u32(0x0001F11F),
|
||||
0x7E51 => char::from_u32(0x0001F120),
|
||||
0x7E52 => char::from_u32(0x0001F121),
|
||||
0x7E53 => char::from_u32(0x0001F122),
|
||||
0x7E54 => char::from_u32(0x0001F123),
|
||||
0x7E55 => char::from_u32(0x0001F124),
|
||||
0x7E56 => char::from_u32(0x0001F125),
|
||||
0x7E57 => char::from_u32(0x0001F126),
|
||||
0x7E58 => char::from_u32(0x0001F127),
|
||||
0x7E59 => char::from_u32(0x0001F128),
|
||||
0x7E5A => char::from_u32(0x0001F129),
|
||||
0x7E5B => char::from_u32(0x00003255),
|
||||
0x7E5C => char::from_u32(0x00003256),
|
||||
0x7E5D => char::from_u32(0x00003257),
|
||||
0x7E5E => char::from_u32(0x00003258),
|
||||
0x7E5F => char::from_u32(0x00003259),
|
||||
0x7E60 => char::from_u32(0x0000325A),
|
||||
0x7E61 => char::from_u32(0x00002460),
|
||||
0x7E62 => char::from_u32(0x00002461),
|
||||
0x7E63 => char::from_u32(0x00002462),
|
||||
0x7E64 => char::from_u32(0x00002463),
|
||||
0x7E65 => char::from_u32(0x00002464),
|
||||
0x7E66 => char::from_u32(0x00002465),
|
||||
0x7E67 => char::from_u32(0x00002466),
|
||||
0x7E68 => char::from_u32(0x00002467),
|
||||
0x7E69 => char::from_u32(0x00002468),
|
||||
0x7E6A => char::from_u32(0x00002469),
|
||||
0x7E6B => char::from_u32(0x0000246A),
|
||||
0x7E6C => char::from_u32(0x0000246B),
|
||||
0x7E6D => char::from_u32(0x0000246C),
|
||||
0x7E6E => char::from_u32(0x0000246D),
|
||||
0x7E6F => char::from_u32(0x0000246E),
|
||||
0x7E70 => char::from_u32(0x0000246F),
|
||||
0x7E71 => char::from_u32(0x00002776),
|
||||
0x7E72 => char::from_u32(0x00002777),
|
||||
0x7E73 => char::from_u32(0x00002778),
|
||||
0x7E74 => char::from_u32(0x00002779),
|
||||
0x7E75 => char::from_u32(0x0000277A),
|
||||
0x7E76 => char::from_u32(0x0000277B),
|
||||
0x7E77 => char::from_u32(0x0000277C),
|
||||
0x7E78 => char::from_u32(0x0000277D),
|
||||
0x7E79 => char::from_u32(0x0000277E),
|
||||
0x7E7A => char::from_u32(0x0000277F),
|
||||
0x7E7B => char::from_u32(0x000024EB),
|
||||
0x7E7C => char::from_u32(0x000024EC),
|
||||
0x7E7D => char::from_u32(0x0000325B),
|
||||
0x7521 => char::from_u32(0x00003402),
|
||||
0x7522 => char::from_u32(0x00020158),
|
||||
0x7523 => char::from_u32(0x00004EFD),
|
||||
0x7524 => char::from_u32(0x00004EFF),
|
||||
0x7525 => char::from_u32(0x00004F9A),
|
||||
0x7526 => char::from_u32(0x00004FC9),
|
||||
0x7527 => char::from_u32(0x0000509C),
|
||||
0x7528 => char::from_u32(0x0000511E),
|
||||
0x7529 => char::from_u32(0x000051BC),
|
||||
0x752A => char::from_u32(0x0000351F),
|
||||
0x752B => char::from_u32(0x00005307),
|
||||
0x752C => char::from_u32(0x00005361),
|
||||
0x752D => char::from_u32(0x0000536C),
|
||||
0x752E => char::from_u32(0x00008A79),
|
||||
0x752F => char::from_u32(0x00020BB7),
|
||||
0x7530 => char::from_u32(0x0000544D),
|
||||
0x7531 => char::from_u32(0x00005496),
|
||||
0x7532 => char::from_u32(0x0000549C),
|
||||
0x7533 => char::from_u32(0x000054A9),
|
||||
0x7534 => char::from_u32(0x0000550E),
|
||||
0x7535 => char::from_u32(0x0000554A),
|
||||
0x7536 => char::from_u32(0x00005672),
|
||||
0x7537 => char::from_u32(0x000056E4),
|
||||
0x7538 => char::from_u32(0x00005733),
|
||||
0x7539 => char::from_u32(0x00005734),
|
||||
0x753A => char::from_u32(0x0000FA10),
|
||||
0x753B => char::from_u32(0x00005880),
|
||||
0x753C => char::from_u32(0x000059E4),
|
||||
0x753D => char::from_u32(0x00005A23),
|
||||
0x753E => char::from_u32(0x00005A55),
|
||||
0x753F => char::from_u32(0x00005BEC),
|
||||
0x7540 => char::from_u32(0x0000FA11),
|
||||
0x7541 => char::from_u32(0x000037E2),
|
||||
0x7542 => char::from_u32(0x00005EAC),
|
||||
0x7543 => char::from_u32(0x00005F34),
|
||||
0x7544 => char::from_u32(0x00005F45),
|
||||
0x7545 => char::from_u32(0x00005FB7),
|
||||
0x7546 => char::from_u32(0x00006017),
|
||||
0x7547 => char::from_u32(0x0000FA6B),
|
||||
0x7548 => char::from_u32(0x00006130),
|
||||
0x7549 => char::from_u32(0x00006624),
|
||||
0x754A => char::from_u32(0x000066C8),
|
||||
0x754B => char::from_u32(0x000066D9),
|
||||
0x754C => char::from_u32(0x000066FA),
|
||||
0x754D => char::from_u32(0x000066FB),
|
||||
0x754E => char::from_u32(0x00006852),
|
||||
0x754F => char::from_u32(0x00009FC4),
|
||||
0x7550 => char::from_u32(0x00006911),
|
||||
0x7551 => char::from_u32(0x0000693B),
|
||||
0x7552 => char::from_u32(0x00006A45),
|
||||
0x7553 => char::from_u32(0x00006A91),
|
||||
0x7554 => char::from_u32(0x00006ADB),
|
||||
0x7555 => char::from_u32(0x000233CC),
|
||||
0x7556 => char::from_u32(0x000233FE),
|
||||
0x7557 => char::from_u32(0x000235C4),
|
||||
0x7558 => char::from_u32(0x00006BF1),
|
||||
0x7559 => char::from_u32(0x00006CE0),
|
||||
0x755A => char::from_u32(0x00006D2E),
|
||||
0x755B => char::from_u32(0x0000FA45),
|
||||
0x755C => char::from_u32(0x00006DBF),
|
||||
0x755D => char::from_u32(0x00006DCA),
|
||||
0x755E => char::from_u32(0x00006DF8),
|
||||
0x755F => char::from_u32(0x0000FA46),
|
||||
0x7560 => char::from_u32(0x00006F5E),
|
||||
0x7561 => char::from_u32(0x00006FF9),
|
||||
0x7562 => char::from_u32(0x00007064),
|
||||
0x7563 => char::from_u32(0x0000FA6C),
|
||||
0x7564 => char::from_u32(0x000242EE),
|
||||
0x7565 => char::from_u32(0x00007147),
|
||||
0x7566 => char::from_u32(0x000071C1),
|
||||
0x7567 => char::from_u32(0x00007200),
|
||||
0x7568 => char::from_u32(0x0000739F),
|
||||
0x7569 => char::from_u32(0x000073A8),
|
||||
0x756A => char::from_u32(0x000073C9),
|
||||
0x756B => char::from_u32(0x000073D6),
|
||||
0x756C => char::from_u32(0x0000741B),
|
||||
0x756D => char::from_u32(0x00007421),
|
||||
0x756E => char::from_u32(0x0000FA4A),
|
||||
0x756F => char::from_u32(0x00007426),
|
||||
0x7570 => char::from_u32(0x0000742A),
|
||||
0x7571 => char::from_u32(0x0000742C),
|
||||
0x7572 => char::from_u32(0x00007439),
|
||||
0x7573 => char::from_u32(0x0000744B),
|
||||
0x7574 => char::from_u32(0x00003EDA),
|
||||
0x7575 => char::from_u32(0x00007575),
|
||||
0x7576 => char::from_u32(0x00007581),
|
||||
0x7577 => char::from_u32(0x00007772),
|
||||
0x7578 => char::from_u32(0x00004093),
|
||||
0x7579 => char::from_u32(0x000078C8),
|
||||
0x757A => char::from_u32(0x000078E0),
|
||||
0x757B => char::from_u32(0x00007947),
|
||||
0x757C => char::from_u32(0x000079AE),
|
||||
0x757D => char::from_u32(0x00009FC6),
|
||||
0x757E => char::from_u32(0x00004103),
|
||||
0x7621 => char::from_u32(0x00009FC5),
|
||||
0x7622 => char::from_u32(0x000079DA),
|
||||
0x7623 => char::from_u32(0x00007A1E),
|
||||
0x7624 => char::from_u32(0x00007B7F),
|
||||
0x7625 => char::from_u32(0x00007C31),
|
||||
0x7626 => char::from_u32(0x00004264),
|
||||
0x7627 => char::from_u32(0x00007D8B),
|
||||
0x7628 => char::from_u32(0x00007FA1),
|
||||
0x7629 => char::from_u32(0x00008118),
|
||||
0x762A => char::from_u32(0x0000813A),
|
||||
0x762B => char::from_u32(0x0000FA6D),
|
||||
0x762C => char::from_u32(0x000082AE),
|
||||
0x762D => char::from_u32(0x0000845B),
|
||||
0x762E => char::from_u32(0x000084DC),
|
||||
0x762F => char::from_u32(0x000084EC),
|
||||
0x7630 => char::from_u32(0x00008559),
|
||||
0x7631 => char::from_u32(0x000085CE),
|
||||
0x7632 => char::from_u32(0x00008755),
|
||||
0x7633 => char::from_u32(0x000087EC),
|
||||
0x7634 => char::from_u32(0x0000880B),
|
||||
0x7635 => char::from_u32(0x000088F5),
|
||||
0x7636 => char::from_u32(0x000089D2),
|
||||
0x7637 => char::from_u32(0x00008AF6),
|
||||
0x7638 => char::from_u32(0x00008DCE),
|
||||
0x7639 => char::from_u32(0x00008FBB),
|
||||
0x763A => char::from_u32(0x00008FF6),
|
||||
0x763B => char::from_u32(0x000090DD),
|
||||
0x763C => char::from_u32(0x00009127),
|
||||
0x763D => char::from_u32(0x0000912D),
|
||||
0x763E => char::from_u32(0x000091B2),
|
||||
0x763F => char::from_u32(0x00009233),
|
||||
0x7640 => char::from_u32(0x00009288),
|
||||
0x7641 => char::from_u32(0x00009321),
|
||||
0x7642 => char::from_u32(0x00009348),
|
||||
0x7643 => char::from_u32(0x00009592),
|
||||
0x7644 => char::from_u32(0x000096DE),
|
||||
0x7645 => char::from_u32(0x00009903),
|
||||
0x7646 => char::from_u32(0x00009940),
|
||||
0x7647 => char::from_u32(0x00009AD9),
|
||||
0x7648 => char::from_u32(0x00009BD6),
|
||||
0x7649 => char::from_u32(0x00009DD7),
|
||||
0x764A => char::from_u32(0x00009EB4),
|
||||
0x764B => char::from_u32(0x00009EB5),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,13 @@
|
||||
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
|
||||
use encoding_rs::EUC_JP;
|
||||
use mirakurun_types::{
|
||||
Program, ProgramGenre, ProgramVideo, ProgramVideoResolution, ProgramVideoType, program_id,
|
||||
Program, ProgramAudio, ProgramGenre, ProgramRelatedItem, ProgramRelatedItemType, ProgramSeries,
|
||||
ProgramVideo, ProgramVideoResolution, ProgramVideoType, program_id,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
arib::decode as decode_arib,
|
||||
filter::SectionAssembler,
|
||||
ts::{Packet, PacketFramer, crc32_mpeg2},
|
||||
};
|
||||
@@ -22,6 +23,7 @@ pub struct EitCollector {
|
||||
framer: PacketFramer,
|
||||
assembler: SectionAssembler,
|
||||
programs: HashMap<u64, Program>,
|
||||
states: HashMap<u64, ProgramState>,
|
||||
sections_seen: HashSet<(u8, u16, u8, u8)>,
|
||||
packet_count: u64,
|
||||
section_count: u64,
|
||||
@@ -40,7 +42,7 @@ impl EitCollector {
|
||||
continue;
|
||||
}
|
||||
for section in self.assembler.push(packet) {
|
||||
if parse_eit_section(§ion, &mut self.programs)
|
||||
if parse_eit_section(§ion, &mut self.programs, &mut self.states)
|
||||
&& self.sections_seen.insert((
|
||||
section[0],
|
||||
u16::from_be_bytes([section[3], section[4]]),
|
||||
@@ -72,7 +74,61 @@ impl EitCollector {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_eit_section(section: &[u8], programs: &mut HashMap<u64, Program>) -> bool {
|
||||
#[derive(Debug, Default)]
|
||||
struct VersionRecord(HashMap<u8, u8>);
|
||||
|
||||
impl VersionRecord {
|
||||
fn should_update(&self, table_id: u8, version: u8) -> bool {
|
||||
if self.0.contains_key(&0x4e) && table_id != 0x4e {
|
||||
return false;
|
||||
}
|
||||
if self.0.contains_key(&0x4f) && !matches!(table_id, 0x4e | 0x4f) {
|
||||
return false;
|
||||
}
|
||||
self.0.get(&table_id).copied() != Some(version)
|
||||
}
|
||||
|
||||
fn mark(&mut self, table_id: u8, version: u8) {
|
||||
self.0.insert(table_id, version);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ProgramState {
|
||||
initialized: bool,
|
||||
present: bool,
|
||||
following: bool,
|
||||
event: VersionRecord,
|
||||
short: VersionRecord,
|
||||
extended: ExtendedState,
|
||||
component: VersionRecord,
|
||||
content: VersionRecord,
|
||||
audio: HashMap<u8, VersionRecord>,
|
||||
audios: BTreeMap<u8, ProgramAudio>,
|
||||
series: VersionRecord,
|
||||
group: HashMap<u8, VersionRecord>,
|
||||
groups: BTreeMap<u8, Vec<ProgramRelatedItem>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ExtendedState {
|
||||
versions: VersionRecord,
|
||||
parts: BTreeMap<u8, Vec<ExtendedItem>>,
|
||||
last_descriptor: Option<u8>,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ExtendedItem {
|
||||
description: Vec<u8>,
|
||||
value: Vec<u8>,
|
||||
}
|
||||
|
||||
fn parse_eit_section(
|
||||
section: &[u8],
|
||||
programs: &mut HashMap<u64, Program>,
|
||||
states: &mut HashMap<u64, ProgramState>,
|
||||
) -> bool {
|
||||
if section.len() < 18
|
||||
|| !matches!(section[0], 0x4e..=0x6f)
|
||||
|| section[5] & 0x01 == 0
|
||||
@@ -80,10 +136,14 @@ fn parse_eit_section(section: &[u8], programs: &mut HashMap<u64, Program>) -> bo
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let table_id = section[0];
|
||||
let version = (section[5] >> 1) & 0x1f;
|
||||
let is_present_following = matches!(section[0], 0x4e | 0x4f);
|
||||
if is_present_following && section[6] > 1 {
|
||||
return false;
|
||||
}
|
||||
let is_present = is_present_following && section[6] == 0;
|
||||
let is_following = is_present_following && section[6] == 1;
|
||||
let service_id = u16::from_be_bytes([section[3], section[4]]);
|
||||
let network_id = u16::from_be_bytes([section[10], section[11]]);
|
||||
let Some(end) = section.len().checked_sub(4) else {
|
||||
@@ -105,36 +165,64 @@ fn parse_eit_section(section: &[u8], programs: &mut HashMap<u64, Program>) -> bo
|
||||
if descriptor_end > end {
|
||||
return false;
|
||||
}
|
||||
if let Some(start_at) = decode_mjd_time(start) {
|
||||
let id = program_id(network_id, service_id, event_id);
|
||||
let program = programs.entry(id).or_insert_with(|| Program {
|
||||
id,
|
||||
event_id,
|
||||
service_id,
|
||||
network_id,
|
||||
start_at,
|
||||
duration: decode_bcd_duration(duration).unwrap_or(1),
|
||||
is_free: section[offset + 10] & 0x10 == 0,
|
||||
name: None,
|
||||
description: None,
|
||||
genres: None,
|
||||
video: None,
|
||||
audios: None,
|
||||
series: None,
|
||||
extended: None,
|
||||
related_items: None,
|
||||
});
|
||||
program.start_at = start_at;
|
||||
program.duration = decode_bcd_duration(duration).unwrap_or(1);
|
||||
program.is_free = section[offset + 10] & 0x10 == 0;
|
||||
parse_descriptors(§ion[offset + 12..descriptor_end], program);
|
||||
let id = program_id(network_id, service_id, event_id);
|
||||
let start_at = decode_mjd_time(start);
|
||||
if !programs.contains_key(&id) && start_at.is_none() {
|
||||
offset = descriptor_end;
|
||||
continue;
|
||||
}
|
||||
let program = programs.entry(id).or_insert_with(|| Program {
|
||||
id,
|
||||
event_id,
|
||||
service_id,
|
||||
network_id,
|
||||
start_at: start_at.unwrap_or_default(),
|
||||
duration: decode_bcd_duration(duration).unwrap_or(1),
|
||||
is_free: section[offset + 10] & 0x10 == 0,
|
||||
name: None,
|
||||
description: None,
|
||||
genres: None,
|
||||
video: None,
|
||||
audios: None,
|
||||
series: None,
|
||||
extended: None,
|
||||
related_items: None,
|
||||
});
|
||||
let state = states.entry(id).or_default();
|
||||
let update_event = !state.initialized
|
||||
|| (!state.present && is_present)
|
||||
|| (!state.following && is_following)
|
||||
|| state.event.should_update(table_id, version);
|
||||
if update_event {
|
||||
if let Some(start_at) = start_at {
|
||||
program.start_at = start_at;
|
||||
program.duration = decode_bcd_duration(duration).unwrap_or(1);
|
||||
program.is_free = section[offset + 10] & 0x10 == 0;
|
||||
}
|
||||
state.initialized = true;
|
||||
state.present = is_present;
|
||||
state.following = is_following;
|
||||
state.event.mark(table_id, version);
|
||||
}
|
||||
parse_descriptors(
|
||||
§ion[offset + 12..descriptor_end],
|
||||
program,
|
||||
state,
|
||||
table_id,
|
||||
version,
|
||||
);
|
||||
offset = descriptor_end;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn parse_descriptors(mut descriptors: &[u8], program: &mut Program) {
|
||||
fn parse_descriptors(
|
||||
mut descriptors: &[u8],
|
||||
program: &mut Program,
|
||||
state: &mut ProgramState,
|
||||
table_id: u8,
|
||||
version: u8,
|
||||
) {
|
||||
while descriptors.len() >= 2 {
|
||||
let length = usize::from(descriptors[1]);
|
||||
let Some(end) = 2usize.checked_add(length) else {
|
||||
@@ -145,43 +233,81 @@ fn parse_descriptors(mut descriptors: &[u8], program: &mut Program) {
|
||||
}
|
||||
let body = &descriptors[2..end];
|
||||
match descriptors[0] {
|
||||
0x4d => parse_short_event(body, program),
|
||||
0x4e => parse_extended_event(body, program),
|
||||
0x50 => parse_video_component(body, program),
|
||||
0x54 => parse_content(body, program),
|
||||
0x4d if state.short.should_update(table_id, version) => {
|
||||
if let Some((name, description)) = parse_short_event(body) {
|
||||
state.short.mark(table_id, version);
|
||||
program.name = Some(name);
|
||||
program.description = Some(description);
|
||||
}
|
||||
}
|
||||
0x4e => parse_extended_event(body, program, &mut state.extended, table_id, version),
|
||||
0x50 if state.component.should_update(table_id, version) => {
|
||||
if let Some(video) = parse_video_component(body) {
|
||||
state.component.mark(table_id, version);
|
||||
program.video = Some(video);
|
||||
}
|
||||
}
|
||||
0x54 if state.content.should_update(table_id, version) => {
|
||||
if let Some(genres) = parse_content(body) {
|
||||
state.content.mark(table_id, version);
|
||||
program.genres = Some(genres);
|
||||
}
|
||||
}
|
||||
0xc4 => parse_audio_component(body, program, state, table_id, version),
|
||||
0xd5 if state.series.should_update(table_id, version) => {
|
||||
if let Some(series) = parse_series(body) {
|
||||
state.series.mark(table_id, version);
|
||||
program.series = Some(series);
|
||||
}
|
||||
}
|
||||
0xd6 => parse_event_group(body, program, state, table_id, version),
|
||||
_ => {}
|
||||
}
|
||||
descriptors = &descriptors[end..];
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_short_event(body: &[u8], program: &mut Program) {
|
||||
fn parse_short_event(body: &[u8]) -> Option<(String, String)> {
|
||||
if body.len() < 5 {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
let name_length = usize::from(body[3]);
|
||||
let Some(text_length_offset) = 4usize.checked_add(name_length) else {
|
||||
return;
|
||||
};
|
||||
let text_length_offset = 4usize.checked_add(name_length)?;
|
||||
if text_length_offset >= body.len() {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
let text_length = usize::from(body[text_length_offset]);
|
||||
let text_start = text_length_offset + 1;
|
||||
let Some(text_end) = text_start.checked_add(text_length) else {
|
||||
return;
|
||||
};
|
||||
let text_end = text_start.checked_add(text_length)?;
|
||||
if text_end > body.len() {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
program.name = Some(decode_arib(&body[4..text_length_offset]));
|
||||
program.description = Some(decode_arib(&body[text_start..text_end]));
|
||||
Some((
|
||||
decode_arib(&body[4..text_length_offset]),
|
||||
decode_arib(&body[text_start..text_end]),
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_extended_event(body: &[u8], program: &mut Program) {
|
||||
fn parse_extended_event(
|
||||
body: &[u8],
|
||||
program: &mut Program,
|
||||
state: &mut ExtendedState,
|
||||
table_id: u8,
|
||||
version: u8,
|
||||
) {
|
||||
if body.len() < 6 {
|
||||
return;
|
||||
}
|
||||
let descriptor_number = body[0] >> 4;
|
||||
let last_descriptor = body[0] & 0x0f;
|
||||
if state.versions.should_update(table_id, version) {
|
||||
state.versions.mark(table_id, version);
|
||||
state.parts.clear();
|
||||
state.last_descriptor = Some(last_descriptor);
|
||||
state.done = false;
|
||||
} else if state.done {
|
||||
return;
|
||||
}
|
||||
let items_length = usize::from(body[4]);
|
||||
let Some(items_end) = 5usize.checked_add(items_length) else {
|
||||
return;
|
||||
@@ -189,44 +315,78 @@ fn parse_extended_event(body: &[u8], program: &mut Program) {
|
||||
if items_end > body.len() {
|
||||
return;
|
||||
}
|
||||
let extended = program.extended.get_or_insert_with(BTreeMap::new);
|
||||
let mut items = &body[5..items_end];
|
||||
let mut previous_key = String::new();
|
||||
let Some(items) = parse_extended_items(&body[5..items_end]) else {
|
||||
return;
|
||||
};
|
||||
state.parts.entry(descriptor_number).or_insert(items);
|
||||
let Some(last) = state.last_descriptor else {
|
||||
return;
|
||||
};
|
||||
if !(0..=last).all(|number| state.parts.contains_key(&number)) {
|
||||
return;
|
||||
}
|
||||
program.extended = Some(build_extended(&state.parts));
|
||||
state.done = true;
|
||||
}
|
||||
|
||||
fn parse_extended_items(mut items: &[u8]) -> Option<Vec<ExtendedItem>> {
|
||||
let mut parsed = Vec::new();
|
||||
while !items.is_empty() {
|
||||
let description_length = usize::from(items[0]);
|
||||
let Some(description_end) = 1usize.checked_add(description_length) else {
|
||||
return;
|
||||
};
|
||||
let description_end = 1usize.checked_add(description_length)?;
|
||||
if description_end >= items.len() {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
let item_length = usize::from(items[description_end]);
|
||||
let item_start = description_end + 1;
|
||||
let Some(item_end) = item_start.checked_add(item_length) else {
|
||||
return;
|
||||
};
|
||||
let item_end = item_start.checked_add(item_length)?;
|
||||
if item_end > items.len() {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
let decoded_key = decode_arib(&items[1..description_end]);
|
||||
let key = if decoded_key.is_empty() {
|
||||
previous_key.clone()
|
||||
} else {
|
||||
previous_key.clone_from(&decoded_key);
|
||||
decoded_key
|
||||
};
|
||||
let value = decode_arib(&items[item_start..item_end]);
|
||||
extended
|
||||
.entry(key)
|
||||
.and_modify(|current| current.push_str(&value))
|
||||
.or_insert(value);
|
||||
parsed.push(ExtendedItem {
|
||||
description: items[1..description_end].to_vec(),
|
||||
value: items[item_start..item_end].to_vec(),
|
||||
});
|
||||
items = &items[item_end..];
|
||||
}
|
||||
Some(parsed)
|
||||
}
|
||||
|
||||
fn parse_video_component(body: &[u8], program: &mut Program) {
|
||||
fn build_extended(parts: &BTreeMap<u8, Vec<ExtendedItem>>) -> BTreeMap<String, String> {
|
||||
let mut buffers: BTreeMap<String, Vec<Vec<u8>>> = BTreeMap::new();
|
||||
let mut current_key = String::new();
|
||||
for items in parts.values() {
|
||||
for item in items {
|
||||
let decoded_key = decode_arib(&item.description);
|
||||
if !decoded_key.is_empty() {
|
||||
current_key = decoded_key;
|
||||
}
|
||||
let values = buffers.entry(current_key.clone()).or_default();
|
||||
if item.description.is_empty() && !values.is_empty() {
|
||||
if let Some(previous) = values.last_mut() {
|
||||
previous.extend_from_slice(&item.value);
|
||||
}
|
||||
} else {
|
||||
values.push(item.value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
buffers
|
||||
.into_iter()
|
||||
.map(|(key, values)| {
|
||||
let value = values
|
||||
.iter()
|
||||
.map(|value| decode_arib(value))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n");
|
||||
(key, value)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_video_component(body: &[u8]) -> Option<ProgramVideo> {
|
||||
if body.len() < 2 {
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
let stream_content = body[0] & 0x0f;
|
||||
let component_type = body[1];
|
||||
@@ -234,7 +394,7 @@ fn parse_video_component(body: &[u8], program: &mut Program) {
|
||||
1 => ProgramVideoType::Mpeg2,
|
||||
5 => ProgramVideoType::H264,
|
||||
9 => ProgramVideoType::H265,
|
||||
_ => return,
|
||||
_ => return None,
|
||||
};
|
||||
let resolution = match component_type {
|
||||
0x01..=0x04 => ProgramVideoResolution::I480,
|
||||
@@ -245,17 +405,17 @@ fn parse_video_component(body: &[u8], program: &mut Program) {
|
||||
0xc1..=0xc4 => ProgramVideoResolution::P720,
|
||||
0xd1..=0xd4 => ProgramVideoResolution::P240,
|
||||
0xe1..=0xe4 => ProgramVideoResolution::P1080,
|
||||
_ => return,
|
||||
_ => return None,
|
||||
};
|
||||
program.video = Some(ProgramVideo {
|
||||
Some(ProgramVideo {
|
||||
video_type,
|
||||
resolution,
|
||||
stream_content,
|
||||
component_type,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_content(body: &[u8], program: &mut Program) {
|
||||
fn parse_content(body: &[u8]) -> Option<Vec<ProgramGenre>> {
|
||||
let genres = body
|
||||
.chunks_exact(2)
|
||||
.map(|content| ProgramGenre {
|
||||
@@ -265,9 +425,133 @@ fn parse_content(body: &[u8], program: &mut Program) {
|
||||
un2: content[1] & 0x0f,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if !genres.is_empty() {
|
||||
program.genres = Some(genres);
|
||||
(!genres.is_empty()).then_some(genres)
|
||||
}
|
||||
|
||||
fn parse_audio_component(
|
||||
body: &[u8],
|
||||
program: &mut Program,
|
||||
state: &mut ProgramState,
|
||||
table_id: u8,
|
||||
version: u8,
|
||||
) {
|
||||
if body.len() < 9 {
|
||||
return;
|
||||
}
|
||||
let component_tag = body[2];
|
||||
let versions = state.audio.entry(component_tag).or_default();
|
||||
if !versions.should_update(table_id, version) {
|
||||
return;
|
||||
}
|
||||
let multilingual = body[5] & 0x80 != 0;
|
||||
if multilingual && body.len() < 12 {
|
||||
return;
|
||||
}
|
||||
let mut langs = vec![language_code(&body[6..9])];
|
||||
if multilingual {
|
||||
langs.push(language_code(&body[9..12]));
|
||||
}
|
||||
let sampling_rate = match (body[5] >> 1) & 0x07 {
|
||||
1 => 16_000,
|
||||
2 => 22_050,
|
||||
3 => 24_000,
|
||||
5 => 32_000,
|
||||
6 => 44_100,
|
||||
7 => 48_000,
|
||||
_ => return,
|
||||
};
|
||||
versions.mark(table_id, version);
|
||||
state.audios.insert(
|
||||
component_tag,
|
||||
ProgramAudio {
|
||||
component_type: body[1],
|
||||
component_tag,
|
||||
is_main: body[5] & 0x40 != 0,
|
||||
sampling_rate,
|
||||
langs,
|
||||
},
|
||||
);
|
||||
program.audios = Some(state.audios.values().cloned().collect());
|
||||
}
|
||||
|
||||
fn language_code(value: &[u8]) -> String {
|
||||
match value {
|
||||
b"jpn" | b"eng" | b"deu" | b"fra" | b"ita" | b"rus" | b"zho" | b"kor" | b"spa" => {
|
||||
String::from_utf8_lossy(value).into_owned()
|
||||
}
|
||||
_ => "etc".into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_series(body: &[u8]) -> Option<ProgramSeries> {
|
||||
if body.len() < 8 {
|
||||
return None;
|
||||
}
|
||||
let expires_at = if body[2] & 0x01 != 0 {
|
||||
decode_mjd_date(&body[3..5]).unwrap_or(-1)
|
||||
} else {
|
||||
-1
|
||||
};
|
||||
let episodes = (u32::from(body[5]) << 16) | (u32::from(body[6]) << 8) | u32::from(body[7]);
|
||||
Some(ProgramSeries {
|
||||
id: u16::from_be_bytes([body[0], body[1]]),
|
||||
repeat: body[2] >> 4,
|
||||
pattern: (body[2] >> 1) & 0x07,
|
||||
expires_at,
|
||||
episode: u16::try_from(episodes >> 12).ok()?,
|
||||
last_episode: u16::try_from(episodes & 0x0fff).ok()?,
|
||||
name: decode_arib(&body[8..]),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_event_group(
|
||||
body: &[u8],
|
||||
program: &mut Program,
|
||||
state: &mut ProgramState,
|
||||
table_id: u8,
|
||||
version: u8,
|
||||
) {
|
||||
let Some(&header) = body.first() else {
|
||||
return;
|
||||
};
|
||||
let group_type = header >> 4;
|
||||
let event_count = usize::from(header & 0x0f);
|
||||
let versions = state.group.entry(group_type).or_default();
|
||||
if !versions.should_update(table_id, version) {
|
||||
return;
|
||||
}
|
||||
let local_end = 1usize.saturating_add(event_count.saturating_mul(4));
|
||||
if local_end > body.len() {
|
||||
return;
|
||||
}
|
||||
let item_type = match group_type {
|
||||
1 => ProgramRelatedItemType::Shared,
|
||||
2 | 4 => ProgramRelatedItemType::Relay,
|
||||
_ => ProgramRelatedItemType::Movement,
|
||||
};
|
||||
let mut items = Vec::new();
|
||||
if matches!(group_type, 4 | 5) {
|
||||
for event in body[local_end..].chunks_exact(8) {
|
||||
items.push(ProgramRelatedItem {
|
||||
item_type,
|
||||
network_id: Some(u16::from_be_bytes([event[0], event[1]])),
|
||||
service_id: u16::from_be_bytes([event[4], event[5]]),
|
||||
event_id: u16::from_be_bytes([event[6], event[7]]),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
for event in body[1..local_end].chunks_exact(4) {
|
||||
items.push(ProgramRelatedItem {
|
||||
item_type,
|
||||
network_id: None,
|
||||
service_id: u16::from_be_bytes([event[0], event[1]]),
|
||||
event_id: u16::from_be_bytes([event[2], event[3]]),
|
||||
});
|
||||
}
|
||||
}
|
||||
versions.mark(table_id, version);
|
||||
state.groups.insert(group_type, items);
|
||||
program.related_items = Some(state.groups.values().flatten().cloned().collect());
|
||||
}
|
||||
|
||||
fn decode_mjd_time(value: &[u8]) -> Option<u64> {
|
||||
@@ -289,6 +573,17 @@ fn decode_mjd_time(value: &[u8]) -> Option<u64> {
|
||||
u64::try_from(unix_seconds.checked_mul(1000)?).ok()
|
||||
}
|
||||
|
||||
fn decode_mjd_date(value: &[u8]) -> Option<i64> {
|
||||
if value.len() != 2 || value.iter().all(|byte| *byte == 0xff) {
|
||||
return None;
|
||||
}
|
||||
let mjd = i64::from(u16::from_be_bytes([value[0], value[1]]));
|
||||
(mjd - 40_587)
|
||||
.checked_mul(86_400)?
|
||||
.checked_sub(9 * 3_600)?
|
||||
.checked_mul(1000)
|
||||
}
|
||||
|
||||
fn decode_bcd_duration(value: &[u8]) -> Option<u64> {
|
||||
if value.len() != 3 || value.iter().all(|byte| *byte == 0xff) {
|
||||
return None;
|
||||
@@ -312,152 +607,12 @@ fn decode_bcd(value: u8) -> Option<u8> {
|
||||
(high <= 9 && low <= 9).then_some(high * 10 + low)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum GraphicSet {
|
||||
Kanji,
|
||||
Alphanumeric,
|
||||
Hiragana,
|
||||
Katakana,
|
||||
}
|
||||
|
||||
pub(crate) fn decode_arib(input: &[u8]) -> String {
|
||||
let mut output = String::new();
|
||||
let mut sets = [
|
||||
GraphicSet::Kanji,
|
||||
GraphicSet::Alphanumeric,
|
||||
GraphicSet::Hiragana,
|
||||
GraphicSet::Katakana,
|
||||
];
|
||||
let mut left = 0usize;
|
||||
let mut right = 2usize;
|
||||
let mut single_shift = None;
|
||||
let mut offset = 0;
|
||||
while offset < input.len() {
|
||||
let byte = input[offset];
|
||||
match byte {
|
||||
0x0e => left = 1,
|
||||
0x0f => left = 0,
|
||||
0x19 => single_shift = Some(2),
|
||||
0x1d => single_shift = Some(3),
|
||||
0x1b => {
|
||||
offset += consume_escape(&input[offset..], &mut sets, &mut left, &mut right)
|
||||
.saturating_sub(1);
|
||||
}
|
||||
0x0d => output.push('\n'),
|
||||
0x20 | 0xa0 => output.push(' '),
|
||||
0x21..=0x7e => {
|
||||
let set = single_shift.take().unwrap_or(left);
|
||||
let consumed = decode_graphic(sets[set], &input[offset..], false, &mut output);
|
||||
offset += consumed.saturating_sub(1);
|
||||
}
|
||||
0xa1..=0xfe => {
|
||||
let consumed = decode_graphic(sets[right], &input[offset..], true, &mut output);
|
||||
offset += consumed.saturating_sub(1);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
offset += 1;
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn decode_graphic(set: GraphicSet, input: &[u8], high_bit: bool, output: &mut String) -> usize {
|
||||
let first = if high_bit { input[0] & 0x7f } else { input[0] };
|
||||
match set {
|
||||
GraphicSet::Kanji => {
|
||||
let Some(second) = input.get(1).copied() else {
|
||||
return 1;
|
||||
};
|
||||
let second = if high_bit { second & 0x7f } else { second };
|
||||
append_euc_jp(&[first | 0x80, second | 0x80], output);
|
||||
2
|
||||
}
|
||||
GraphicSet::Alphanumeric => {
|
||||
output.push(char::from(first));
|
||||
1
|
||||
}
|
||||
GraphicSet::Hiragana => {
|
||||
append_euc_jp(&[0xa4, first | 0x80], output);
|
||||
1
|
||||
}
|
||||
GraphicSet::Katakana => {
|
||||
append_euc_jp(&[0xa5, first | 0x80], output);
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn append_euc_jp(bytes: &[u8], output: &mut String) {
|
||||
let (decoded, had_errors) = EUC_JP.decode_without_bom_handling(bytes);
|
||||
if had_errors {
|
||||
output.push('\u{fffd}');
|
||||
} else {
|
||||
output.push_str(&decoded);
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_escape(
|
||||
input: &[u8],
|
||||
sets: &mut [GraphicSet; 4],
|
||||
left: &mut usize,
|
||||
right: &mut usize,
|
||||
) -> usize {
|
||||
let Some(second) = input.get(1).copied() else {
|
||||
return 1;
|
||||
};
|
||||
match second {
|
||||
0x6e => *left = 2,
|
||||
0x6f => *left = 3,
|
||||
0x7c => *right = 3,
|
||||
0x7d => *right = 2,
|
||||
0x7e => *right = 1,
|
||||
0x28..=0x2b => {
|
||||
let Some(code) = input.get(2).copied() else {
|
||||
return 2;
|
||||
};
|
||||
if let Some(set) = graphic_set(code) {
|
||||
sets[usize::from(second - 0x28)] = set;
|
||||
}
|
||||
return 3;
|
||||
}
|
||||
0x24 => {
|
||||
let Some(third) = input.get(2).copied() else {
|
||||
return 2;
|
||||
};
|
||||
if matches!(third, 0x28..=0x2b) {
|
||||
let Some(code) = input.get(3).copied() else {
|
||||
return 3;
|
||||
};
|
||||
if let Some(set) = graphic_set(code) {
|
||||
sets[usize::from(third - 0x28)] = set;
|
||||
}
|
||||
return 4;
|
||||
}
|
||||
if let Some(set) = graphic_set(third) {
|
||||
sets[0] = set;
|
||||
}
|
||||
return 3;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
2
|
||||
}
|
||||
|
||||
const fn graphic_set(code: u8) -> Option<GraphicSet> {
|
||||
match code {
|
||||
0x42 | 0x39 => Some(GraphicSet::Kanji),
|
||||
0x4a | 0x36 => Some(GraphicSet::Alphanumeric),
|
||||
0x30 | 0x37 => Some(GraphicSet::Hiragana),
|
||||
0x31 | 0x38 | 0x49 => Some(GraphicSet::Katakana),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::ts::{PACKET_SIZE, crc32_mpeg2};
|
||||
use mirakurun_types::ProgramRelatedItemType;
|
||||
|
||||
use super::{EitCollector, decode_arib, decode_bcd_duration, decode_mjd_time};
|
||||
use super::{EitCollector, VersionRecord, decode_arib, decode_bcd_duration, decode_mjd_time};
|
||||
|
||||
#[test]
|
||||
fn decodes_common_arib_character_sets() {
|
||||
@@ -506,4 +661,108 @@ mod tests {
|
||||
assert_eq!(programs[0].duration, 3_600_000);
|
||||
assert!(programs[0].is_free);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_audio_series_and_related_program_descriptors() {
|
||||
let mut descriptors = vec![
|
||||
0xc4, 0x09, 0x02, 0x03, 0x10, 0x0f, 0xff, 0x4e, b'j', b'p', b'n',
|
||||
];
|
||||
let series_name = [0x0e, b'S', b'e', b'r', b'i', b'e', b's'];
|
||||
descriptors.extend_from_slice(&[
|
||||
0xd5,
|
||||
u8::try_from(8 + series_name.len()).unwrap_or(0),
|
||||
0x12,
|
||||
0x34,
|
||||
0x14,
|
||||
0xff,
|
||||
0xff,
|
||||
0x00,
|
||||
0x50,
|
||||
0x0c,
|
||||
]);
|
||||
descriptors.extend_from_slice(&series_name);
|
||||
descriptors.extend_from_slice(&[0xd6, 0x05, 0x11, 0x00, 0x65, 0x00, 0x02]);
|
||||
|
||||
let packet = eit_packet(0x4e, 0, &descriptors);
|
||||
let mut collector = EitCollector::default();
|
||||
collector.push(&packet);
|
||||
let programs = collector.into_programs();
|
||||
let program = &programs[0];
|
||||
let audio = &program.audios.as_ref().expect("audio descriptor")[0];
|
||||
assert_eq!(audio.component_type, 0x03);
|
||||
assert_eq!(audio.component_tag, 0x10);
|
||||
assert!(audio.is_main);
|
||||
assert_eq!(audio.sampling_rate, 48_000);
|
||||
assert_eq!(audio.langs, ["jpn"]);
|
||||
let series = program.series.as_ref().expect("series descriptor");
|
||||
assert_eq!(series.id, 0x1234);
|
||||
assert_eq!(series.repeat, 1);
|
||||
assert_eq!(series.pattern, 2);
|
||||
assert_eq!(series.episode, 5);
|
||||
assert_eq!(series.last_episode, 12);
|
||||
assert_eq!(series.name, "Series");
|
||||
let related = &program.related_items.as_ref().expect("event group")[0];
|
||||
assert_eq!(related.item_type, ProgramRelatedItemType::Shared);
|
||||
assert_eq!(related.network_id, None);
|
||||
assert_eq!(related.service_id, 101);
|
||||
assert_eq!(related.event_id, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn present_following_versions_take_priority_over_schedule_tables() {
|
||||
let mut versions = VersionRecord::default();
|
||||
assert!(versions.should_update(0x50, 1));
|
||||
versions.mark(0x50, 1);
|
||||
assert!(versions.should_update(0x4e, 1));
|
||||
versions.mark(0x4e, 1);
|
||||
assert!(!versions.should_update(0x50, 2));
|
||||
assert!(versions.should_update(0x4e, 2));
|
||||
}
|
||||
|
||||
fn eit_packet(table_id: u8, version: u8, descriptors: &[u8]) -> [u8; PACKET_SIZE] {
|
||||
let descriptor_length = descriptors.len();
|
||||
let mut section = vec![
|
||||
table_id,
|
||||
0xb0,
|
||||
0x00,
|
||||
0x00,
|
||||
0x65,
|
||||
0xc1 | ((version & 0x1f) << 1),
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x7f,
|
||||
0xf0,
|
||||
0x00,
|
||||
table_id,
|
||||
0x00,
|
||||
0x01,
|
||||
0x9e,
|
||||
0x8b,
|
||||
0x09,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x80 | u8::try_from(descriptor_length >> 8).unwrap_or(0),
|
||||
u8::try_from(descriptor_length & 0xff).unwrap_or(0),
|
||||
];
|
||||
section.extend_from_slice(descriptors);
|
||||
let section_length = section.len() - 3 + 4;
|
||||
section[1] = 0xb0 | u8::try_from(section_length >> 8).unwrap_or(0);
|
||||
section[2] = u8::try_from(section_length & 0xff).unwrap_or(0);
|
||||
let crc = crc32_mpeg2(§ion);
|
||||
section.extend_from_slice(&crc.to_be_bytes());
|
||||
|
||||
let mut packet = [0xff; PACKET_SIZE];
|
||||
packet[0] = 0x47;
|
||||
packet[1] = 0x40;
|
||||
packet[2] = 0x12;
|
||||
packet[3] = 0x10;
|
||||
packet[4] = 0;
|
||||
packet[5..5 + section.len()].copy_from_slice(§ion);
|
||||
packet
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
mod arib;
|
||||
pub mod config;
|
||||
pub mod epg;
|
||||
pub mod error;
|
||||
pub mod filter;
|
||||
pub mod logo;
|
||||
pub mod persistence;
|
||||
pub mod service;
|
||||
pub mod ts;
|
||||
|
||||
637
crates/mirakurun-core/src/logo.rs
Normal file
637
crates/mirakurun-core/src/logo.rs
Normal file
@@ -0,0 +1,637 @@
|
||||
// Copyright 2026 Mirakurun contributors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::{
|
||||
filter::SectionAssembler,
|
||||
ts::{Packet, PacketFramer, crc32_mpeg2},
|
||||
};
|
||||
|
||||
const PID_CDT: u16 = 0x0029;
|
||||
const TABLE_ID_CDT: u8 = 0xc8;
|
||||
const TABLE_ID_DII: u8 = 0x3b;
|
||||
const TABLE_ID_DDB: u8 = 0x3c;
|
||||
const MAX_LOGO_MODULE_SIZE: usize = 64 * 1024 * 1024;
|
||||
|
||||
/// A decoded type-5 broadcaster logo and the services which reference it.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LogoData {
|
||||
pub network_id: u16,
|
||||
pub logo_id: u16,
|
||||
pub services: Vec<(u16, u16)>,
|
||||
pub png: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Download {
|
||||
module_id: u16,
|
||||
module_version: u8,
|
||||
block_size: usize,
|
||||
bytes: Vec<u8>,
|
||||
received_blocks: HashSet<u16>,
|
||||
received_bytes: usize,
|
||||
}
|
||||
|
||||
/// Collects terrestrial CDT and satellite DSM-CC logo data from a transport stream.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct LogoCollector {
|
||||
framer: PacketFramer,
|
||||
cdt_assembler: SectionAssembler,
|
||||
dsmcc_assemblers: HashMap<u16, SectionAssembler>,
|
||||
downloads: HashMap<u32, Download>,
|
||||
logos: HashMap<(u16, u16), LogoData>,
|
||||
}
|
||||
|
||||
impl LogoCollector {
|
||||
pub fn push(&mut self, chunk: &[u8]) {
|
||||
let mut packets = Vec::new();
|
||||
self.framer.push(chunk, |packet| packets.push(*packet));
|
||||
for packet_bytes in packets {
|
||||
let Ok(packet) = Packet::new(&packet_bytes) else {
|
||||
continue;
|
||||
};
|
||||
if packet.pid() == PID_CDT {
|
||||
for section in self.cdt_assembler.push(packet) {
|
||||
if let Some(logo) = parse_cdt(§ion) {
|
||||
self.insert_logo(logo);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let pid = packet.pid();
|
||||
if !self.dsmcc_assemblers.contains_key(&pid) && !is_dsmcc_start(packet) {
|
||||
continue;
|
||||
}
|
||||
let sections = self.dsmcc_assemblers.entry(pid).or_default().push(packet);
|
||||
for section in sections {
|
||||
self.push_dsmcc(§ion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn has_logo(&self) -> bool {
|
||||
!self.logos.is_empty()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn contains(&self, network_id: u16, logo_id: u16) -> bool {
|
||||
self.logos.contains_key(&(network_id, logo_id))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn into_logos(self) -> Vec<LogoData> {
|
||||
let mut logos = self.logos.into_values().collect::<Vec<_>>();
|
||||
logos.sort_by_key(|logo| (logo.network_id, logo.logo_id));
|
||||
logos
|
||||
}
|
||||
|
||||
fn push_dsmcc(&mut self, section: &[u8]) {
|
||||
if section.len() < 12 || crc32_mpeg2(section) != 0 || section[5] & 0x01 == 0 {
|
||||
return;
|
||||
}
|
||||
match section[0] {
|
||||
TABLE_ID_DII => {
|
||||
if let Some((download_id, download)) = parse_dii(section) {
|
||||
self.downloads.entry(download_id).or_insert(download);
|
||||
}
|
||||
}
|
||||
TABLE_ID_DDB => {
|
||||
let Some(block) = parse_ddb(section) else {
|
||||
return;
|
||||
};
|
||||
let Some(download) = self.downloads.get_mut(&block.download_id) else {
|
||||
return;
|
||||
};
|
||||
if download.module_id != block.module_id
|
||||
|| download.module_version != block.module_version
|
||||
|| !download.received_blocks.insert(block.block_number)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let offset = download
|
||||
.block_size
|
||||
.saturating_mul(usize::from(block.block_number));
|
||||
if offset >= download.bytes.len() {
|
||||
return;
|
||||
}
|
||||
let copy_len = block.data.len().min(download.bytes.len() - offset);
|
||||
download.bytes[offset..offset + copy_len].copy_from_slice(&block.data[..copy_len]);
|
||||
download.received_bytes = download.received_bytes.saturating_add(copy_len);
|
||||
if download.received_bytes < download.bytes.len() {
|
||||
return;
|
||||
}
|
||||
let Some(download) = self.downloads.remove(&block.download_id) else {
|
||||
return;
|
||||
};
|
||||
for logo in parse_logo_module(&download.bytes) {
|
||||
self.insert_logo(logo);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_logo(&mut self, mut logo: LogoData) {
|
||||
let key = (logo.network_id, logo.logo_id);
|
||||
if let Some(previous) = self.logos.remove(&key) {
|
||||
logo.services.extend(previous.services);
|
||||
logo.services.sort_unstable();
|
||||
logo.services.dedup();
|
||||
}
|
||||
self.logos.insert(key, logo);
|
||||
}
|
||||
}
|
||||
|
||||
fn is_dsmcc_start(packet: Packet<'_>) -> bool {
|
||||
if !packet.payload_unit_start() {
|
||||
return false;
|
||||
}
|
||||
let Some(payload) = packet.payload() else {
|
||||
return false;
|
||||
};
|
||||
let Some(table_offset) = payload
|
||||
.first()
|
||||
.copied()
|
||||
.map(usize::from)
|
||||
.and_then(|pointer| 1usize.checked_add(pointer))
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
payload
|
||||
.get(table_offset)
|
||||
.is_some_and(|table_id| matches!(*table_id, TABLE_ID_DII | TABLE_ID_DDB))
|
||||
}
|
||||
|
||||
fn parse_cdt(section: &[u8]) -> Option<LogoData> {
|
||||
if section.len() < 24
|
||||
|| section[0] != TABLE_ID_CDT
|
||||
|| section[5] & 0x01 == 0
|
||||
|| crc32_mpeg2(section) != 0
|
||||
|| section[10] != 0x01
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let network_id = u16::from_be_bytes([section[8], section[9]]);
|
||||
let descriptors_length = (usize::from(section[11] & 0x0f) << 8) | usize::from(section[12]);
|
||||
let module_start = 13usize.checked_add(descriptors_length)?;
|
||||
let module_end = section.len().checked_sub(4)?;
|
||||
let module = section.get(module_start..module_end)?;
|
||||
if module.len() < 7 || module[0] != 0x05 {
|
||||
return None;
|
||||
}
|
||||
let logo_id = (u16::from(module[1] & 0x01) << 8) | u16::from(module[2]);
|
||||
let data_size = usize::from(u16::from_be_bytes([module[5], module[6]]));
|
||||
let data_end = 7usize.checked_add(data_size)?;
|
||||
let png = decode_indexed_png(module.get(7..data_end)?)?;
|
||||
Some(LogoData {
|
||||
network_id,
|
||||
logo_id,
|
||||
services: Vec::new(),
|
||||
png,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_dii(section: &[u8]) -> Option<(u32, Download)> {
|
||||
if section.first().copied() != Some(TABLE_ID_DII) {
|
||||
return None;
|
||||
}
|
||||
let body = section.get(8..section.len().checked_sub(4)?)?;
|
||||
if body.len() < 34 || u16::from_be_bytes([body[2], body[3]]) != 0x1002 {
|
||||
return None;
|
||||
}
|
||||
let adaptation_length = usize::from(body[9]);
|
||||
let mut offset = 12usize.checked_add(adaptation_length)?;
|
||||
let download_id = read_u32(body, offset)?;
|
||||
offset += 4;
|
||||
let block_size = usize::from(read_u16(body, offset)?);
|
||||
offset += 12;
|
||||
let compatibility_length = usize::from(read_u16(body, offset)?);
|
||||
offset = offset.checked_add(2)?.checked_add(compatibility_length)?;
|
||||
let module_count = usize::from(read_u16(body, offset)?);
|
||||
offset += 2;
|
||||
for _ in 0..module_count {
|
||||
let module_id = read_u16(body, offset)?;
|
||||
let module_size = usize::try_from(read_u32(body, offset + 2)?).ok()?;
|
||||
let module_version = *body.get(offset + 6)?;
|
||||
let info_length = usize::from(*body.get(offset + 7)?);
|
||||
let info_start = offset + 8;
|
||||
let info_end = info_start.checked_add(info_length)?;
|
||||
let info = body.get(info_start..info_end)?;
|
||||
if module_size <= MAX_LOGO_MODULE_SIZE && is_logo_module(info) {
|
||||
return Some((
|
||||
download_id,
|
||||
Download {
|
||||
module_id,
|
||||
module_version,
|
||||
block_size: block_size.max(1),
|
||||
bytes: vec![0; module_size],
|
||||
received_blocks: HashSet::new(),
|
||||
received_bytes: 0,
|
||||
},
|
||||
));
|
||||
}
|
||||
offset = info_end;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn is_logo_module(mut descriptors: &[u8]) -> bool {
|
||||
while descriptors.len() >= 2 {
|
||||
let end = 2usize.saturating_add(usize::from(descriptors[1]));
|
||||
if end > descriptors.len() {
|
||||
return false;
|
||||
}
|
||||
if descriptors[0] == 0x02 && matches!(&descriptors[2..end], b"LOGO-05" | b"CS_LOGO-05") {
|
||||
return true;
|
||||
}
|
||||
descriptors = &descriptors[end..];
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
struct DownloadBlock<'a> {
|
||||
download_id: u32,
|
||||
module_id: u16,
|
||||
module_version: u8,
|
||||
block_number: u16,
|
||||
data: &'a [u8],
|
||||
}
|
||||
|
||||
fn parse_ddb(section: &[u8]) -> Option<DownloadBlock<'_>> {
|
||||
if section.first().copied() != Some(TABLE_ID_DDB) {
|
||||
return None;
|
||||
}
|
||||
let body = section.get(8..section.len().checked_sub(4)?)?;
|
||||
if body.len() < 18 || u16::from_be_bytes([body[2], body[3]]) != 0x1003 {
|
||||
return None;
|
||||
}
|
||||
let download_id = read_u32(body, 4)?;
|
||||
let adaptation_length = usize::from(body[9]);
|
||||
let offset = 12usize.checked_add(adaptation_length)?;
|
||||
Some(DownloadBlock {
|
||||
download_id,
|
||||
module_id: read_u16(body, offset)?,
|
||||
module_version: *body.get(offset + 2)?,
|
||||
block_number: read_u16(body, offset + 4)?,
|
||||
data: body.get(offset + 6..)?,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_logo_module(module: &[u8]) -> Vec<LogoData> {
|
||||
if module.len() < 3 || module[0] != 0x05 {
|
||||
return Vec::new();
|
||||
}
|
||||
let count = usize::from(u16::from_be_bytes([module[1], module[2]]));
|
||||
let mut offset = 3;
|
||||
let mut output = Vec::new();
|
||||
for _ in 0..count {
|
||||
let Some(logo_bits) = module.get(offset..offset + 2) else {
|
||||
break;
|
||||
};
|
||||
let logo_id = (u16::from(logo_bits[0] & 0x01) << 8) | u16::from(logo_bits[1]);
|
||||
let Some(&service_count) = module.get(offset + 2) else {
|
||||
break;
|
||||
};
|
||||
offset += 3;
|
||||
let mut services = Vec::new();
|
||||
for _ in 0..service_count {
|
||||
let Some(service) = module.get(offset..offset + 6) else {
|
||||
return output;
|
||||
};
|
||||
services.push((
|
||||
u16::from_be_bytes([service[0], service[1]]),
|
||||
u16::from_be_bytes([service[4], service[5]]),
|
||||
));
|
||||
offset += 6;
|
||||
}
|
||||
let Some(data_size) = module.get(offset..offset + 2) else {
|
||||
break;
|
||||
};
|
||||
let data_size = usize::from(u16::from_be_bytes([data_size[0], data_size[1]]));
|
||||
offset += 2;
|
||||
let Some(data) = module.get(offset..offset.saturating_add(data_size)) else {
|
||||
break;
|
||||
};
|
||||
offset += data_size;
|
||||
let Some(png) = decode_indexed_png(data) else {
|
||||
continue;
|
||||
};
|
||||
for network_id in services
|
||||
.iter()
|
||||
.map(|(network_id, _)| *network_id)
|
||||
.collect::<HashSet<_>>()
|
||||
{
|
||||
output.push(LogoData {
|
||||
network_id,
|
||||
logo_id,
|
||||
services: services
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|(service_network_id, _)| *service_network_id == network_id)
|
||||
.collect(),
|
||||
png: png.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn decode_indexed_png(data: &[u8]) -> Option<Vec<u8>> {
|
||||
const PNG_SIGNATURE: &[u8; 8] = b"\x89PNG\r\n\x1a\n";
|
||||
if data.len() < 33 || data.get(..8)? != PNG_SIGNATURE || data.get(25).copied()? != 3 {
|
||||
return None;
|
||||
}
|
||||
let palette = logo_palette();
|
||||
let mut plte = Vec::with_capacity(palette.len() * 3);
|
||||
let mut trns = Vec::with_capacity(palette.len());
|
||||
for [red, green, blue, alpha] in palette {
|
||||
plte.extend_from_slice(&[red, green, blue]);
|
||||
trns.push(alpha);
|
||||
}
|
||||
let plte = png_chunk(*b"PLTE", &plte);
|
||||
let trns = png_chunk(*b"tRNS", &trns);
|
||||
let mut png = Vec::with_capacity(data.len() + plte.len() + trns.len());
|
||||
png.extend_from_slice(&data[..33]);
|
||||
png.extend_from_slice(&plte);
|
||||
png.extend_from_slice(&trns);
|
||||
png.extend_from_slice(&data[33..]);
|
||||
Some(png)
|
||||
}
|
||||
|
||||
fn png_chunk(kind: [u8; 4], data: &[u8]) -> Vec<u8> {
|
||||
let mut chunk = Vec::with_capacity(12 + data.len());
|
||||
chunk.extend_from_slice(&u32::try_from(data.len()).unwrap_or(u32::MAX).to_be_bytes());
|
||||
chunk.extend_from_slice(&kind);
|
||||
chunk.extend_from_slice(data);
|
||||
let checksum = crc32_ieee(&chunk[4..]);
|
||||
chunk.extend_from_slice(&checksum.to_be_bytes());
|
||||
chunk
|
||||
}
|
||||
|
||||
fn crc32_ieee(data: &[u8]) -> u32 {
|
||||
let mut crc = u32::MAX;
|
||||
for byte in data {
|
||||
crc ^= u32::from(*byte);
|
||||
for _ in 0..8 {
|
||||
crc = (crc >> 1) ^ (0xedb8_8320 & 0u32.wrapping_sub(crc & 1));
|
||||
}
|
||||
}
|
||||
!crc
|
||||
}
|
||||
|
||||
fn logo_palette() -> Vec<[u8; 4]> {
|
||||
const COLORS: [[u8; 3]; 65] = [
|
||||
[0, 0, 0],
|
||||
[255, 0, 0],
|
||||
[0, 255, 0],
|
||||
[255, 255, 0],
|
||||
[0, 0, 255],
|
||||
[255, 0, 255],
|
||||
[0, 255, 255],
|
||||
[255, 255, 255],
|
||||
[0, 0, 0],
|
||||
[170, 0, 0],
|
||||
[0, 170, 0],
|
||||
[170, 170, 0],
|
||||
[0, 0, 170],
|
||||
[170, 0, 170],
|
||||
[0, 170, 170],
|
||||
[170, 170, 170],
|
||||
[0, 0, 85],
|
||||
[0, 85, 0],
|
||||
[0, 85, 85],
|
||||
[0, 85, 170],
|
||||
[0, 85, 255],
|
||||
[0, 170, 85],
|
||||
[0, 170, 255],
|
||||
[0, 255, 85],
|
||||
[0, 255, 170],
|
||||
[85, 0, 0],
|
||||
[85, 0, 85],
|
||||
[85, 0, 170],
|
||||
[85, 0, 255],
|
||||
[85, 85, 0],
|
||||
[85, 85, 85],
|
||||
[85, 85, 170],
|
||||
[85, 85, 255],
|
||||
[85, 170, 0],
|
||||
[85, 170, 85],
|
||||
[85, 170, 170],
|
||||
[85, 170, 255],
|
||||
[85, 255, 0],
|
||||
[85, 255, 85],
|
||||
[85, 255, 170],
|
||||
[85, 255, 255],
|
||||
[170, 0, 85],
|
||||
[170, 0, 255],
|
||||
[170, 85, 0],
|
||||
[170, 85, 85],
|
||||
[170, 85, 170],
|
||||
[170, 85, 255],
|
||||
[170, 170, 85],
|
||||
[170, 170, 255],
|
||||
[170, 255, 0],
|
||||
[170, 255, 85],
|
||||
[170, 255, 170],
|
||||
[170, 255, 255],
|
||||
[255, 0, 85],
|
||||
[255, 0, 255],
|
||||
[255, 85, 0],
|
||||
[255, 85, 85],
|
||||
[255, 85, 170],
|
||||
[255, 85, 255],
|
||||
[255, 170, 0],
|
||||
[255, 170, 85],
|
||||
[255, 170, 170],
|
||||
[255, 170, 255],
|
||||
[255, 255, 85],
|
||||
[255, 255, 255],
|
||||
];
|
||||
let mut palette = Vec::with_capacity(128);
|
||||
palette.extend(COLORS.into_iter().enumerate().map(|(index, color)| {
|
||||
[
|
||||
color[0],
|
||||
color[1],
|
||||
color[2],
|
||||
if index == 8 { 0 } else { 255 },
|
||||
]
|
||||
}));
|
||||
palette.extend(
|
||||
COLORS
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter(|(index, _)| *index != 8)
|
||||
.map(|(_, color)| [color[0], color[1], color[2], 128]),
|
||||
);
|
||||
palette
|
||||
}
|
||||
|
||||
fn read_u16(bytes: &[u8], offset: usize) -> Option<u16> {
|
||||
Some(u16::from_be_bytes([
|
||||
*bytes.get(offset)?,
|
||||
*bytes.get(offset + 1)?,
|
||||
]))
|
||||
}
|
||||
|
||||
fn read_u32(bytes: &[u8], offset: usize) -> Option<u32> {
|
||||
Some(u32::from_be_bytes([
|
||||
*bytes.get(offset)?,
|
||||
*bytes.get(offset + 1)?,
|
||||
*bytes.get(offset + 2)?,
|
||||
*bytes.get(offset + 3)?,
|
||||
]))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::ts::{PACKET_SIZE, crc32_mpeg2};
|
||||
|
||||
use super::{LogoCollector, decode_indexed_png, parse_logo_module};
|
||||
|
||||
fn indexed_png() -> Vec<u8> {
|
||||
let mut png = vec![0; 33];
|
||||
png[..8].copy_from_slice(b"\x89PNG\r\n\x1a\n");
|
||||
png[8..12].copy_from_slice(&13u32.to_be_bytes());
|
||||
png[12..16].copy_from_slice(b"IHDR");
|
||||
png[25] = 3;
|
||||
png.extend_from_slice(&[0, 0, 0, 0, b'I', b'E', b'N', b'D', 0, 0, 0, 0]);
|
||||
png
|
||||
}
|
||||
|
||||
fn finish_section(mut section: Vec<u8>) -> Vec<u8> {
|
||||
let section_length = section.len() - 3 + 4;
|
||||
section[1] = 0xf0 | u8::try_from(section_length >> 8).expect("section length");
|
||||
section[2] = u8::try_from(section_length & 0xff).expect("section length");
|
||||
let crc = crc32_mpeg2(§ion);
|
||||
section.extend_from_slice(&crc.to_be_bytes());
|
||||
section
|
||||
}
|
||||
|
||||
fn logo_module() -> Vec<u8> {
|
||||
let source = indexed_png();
|
||||
let mut module = vec![
|
||||
0x05, 0x00, 0x01, 0x00, 0x09, 0x01, 0x00, 0x04, 0x00, 0x01, 0x00, 0x65,
|
||||
];
|
||||
module.extend_from_slice(
|
||||
&u16::try_from(source.len())
|
||||
.expect("logo data length")
|
||||
.to_be_bytes(),
|
||||
);
|
||||
module.extend_from_slice(&source);
|
||||
module
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn injects_arib_palette_into_indexed_png() {
|
||||
let decoded = decode_indexed_png(&indexed_png()).expect("decode PNG");
|
||||
assert_eq!(&decoded[37..41], b"PLTE");
|
||||
assert_eq!(&decoded[436..440], b"tRNS");
|
||||
assert_eq!(decoded.len(), indexed_png().len() + 399 + 141);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collects_type_five_logo_from_cdt() {
|
||||
let source = indexed_png();
|
||||
let mut section = vec![
|
||||
0xc8, 0xf0, 0x00, 0x00, 0x01, 0xc1, 0x00, 0x00, 0x7f, 0xf0, 0x01, 0xf0, 0x00, 0x05,
|
||||
0x00, 0x07, 0x00, 0x01,
|
||||
];
|
||||
section.extend_from_slice(
|
||||
&u16::try_from(source.len())
|
||||
.expect("logo data length")
|
||||
.to_be_bytes(),
|
||||
);
|
||||
section.extend_from_slice(&source);
|
||||
let section = finish_section(section);
|
||||
let mut packet = [0xff; PACKET_SIZE];
|
||||
packet[0] = 0x47;
|
||||
packet[1] = 0x40;
|
||||
packet[2] = 0x29;
|
||||
packet[3] = 0x10;
|
||||
packet[4] = 0;
|
||||
packet[5..5 + section.len()].copy_from_slice(§ion);
|
||||
|
||||
let mut collector = LogoCollector::default();
|
||||
collector.push(&packet);
|
||||
let logos = collector.into_logos();
|
||||
assert_eq!(logos.len(), 1);
|
||||
assert_eq!(logos[0].network_id, 0x7ff0);
|
||||
assert_eq!(logos[0].logo_id, 7);
|
||||
assert_eq!(&logos[0].png[37..41], b"PLTE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_satellite_logo_service_associations() {
|
||||
let logos = parse_logo_module(&logo_module());
|
||||
assert_eq!(logos.len(), 1);
|
||||
assert_eq!(logos[0].network_id, 4);
|
||||
assert_eq!(logos[0].logo_id, 9);
|
||||
assert_eq!(logos[0].services, vec![(4, 101)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reassembles_satellite_logo_from_dii_and_ddb() {
|
||||
let module = logo_module();
|
||||
let download_id = 0x1122_3344_u32;
|
||||
let mut dii_body = vec![
|
||||
0x11, 0x03, 0x10, 0x02, 0x00, 0x00, 0x00, 0x01, 0xff, 0x00, 0x00, 0x00,
|
||||
];
|
||||
dii_body.extend_from_slice(&download_id.to_be_bytes());
|
||||
dii_body.extend_from_slice(&4066_u16.to_be_bytes());
|
||||
dii_body.extend_from_slice(&[0, 0]);
|
||||
dii_body.extend_from_slice(&[0; 8]);
|
||||
dii_body.extend_from_slice(&0_u16.to_be_bytes());
|
||||
dii_body.extend_from_slice(&1_u16.to_be_bytes());
|
||||
dii_body.extend_from_slice(&1_u16.to_be_bytes());
|
||||
dii_body.extend_from_slice(
|
||||
&u32::try_from(module.len())
|
||||
.expect("module size")
|
||||
.to_be_bytes(),
|
||||
);
|
||||
dii_body.extend_from_slice(&[1, 9, 0x02, 7]);
|
||||
dii_body.extend_from_slice(b"LOGO-05");
|
||||
dii_body.extend_from_slice(&0_u16.to_be_bytes());
|
||||
let mut dii = vec![0x3b, 0xf0, 0x00, 0x00, 0x00, 0xc1, 0x00, 0x00];
|
||||
dii.extend_from_slice(&dii_body);
|
||||
let dii = finish_section(dii);
|
||||
|
||||
let message_length = u16::try_from(module.len() + 6).expect("DDB message size");
|
||||
let mut ddb_body = vec![
|
||||
0x11,
|
||||
0x03,
|
||||
0x10,
|
||||
0x03,
|
||||
0x11,
|
||||
0x22,
|
||||
0x33,
|
||||
0x44,
|
||||
0xff,
|
||||
0x00,
|
||||
(message_length >> 8) as u8,
|
||||
(message_length & 0xff) as u8,
|
||||
0x00,
|
||||
0x01,
|
||||
0x01,
|
||||
0xff,
|
||||
0x00,
|
||||
0x00,
|
||||
];
|
||||
ddb_body.extend_from_slice(&module);
|
||||
let mut ddb = vec![0x3c, 0xf0, 0x00, 0x00, 0x00, 0xc1, 0x00, 0x00];
|
||||
ddb.extend_from_slice(&ddb_body);
|
||||
let ddb = finish_section(ddb);
|
||||
|
||||
let mut collector = LogoCollector::default();
|
||||
collector.push_dsmcc(&dii);
|
||||
collector.push_dsmcc(&ddb);
|
||||
let logos = collector.into_logos();
|
||||
assert_eq!(logos.len(), 1);
|
||||
assert_eq!(logos[0].network_id, 4);
|
||||
assert_eq!(logos[0].logo_id, 9);
|
||||
}
|
||||
}
|
||||
@@ -8,22 +8,26 @@ use std::collections::{HashMap, HashSet};
|
||||
use mirakurun_types::{Channel, ConfigChannel, Service, service_item_id};
|
||||
|
||||
use crate::{
|
||||
epg::decode_arib,
|
||||
arib::decode as decode_arib,
|
||||
filter::SectionAssembler,
|
||||
ts::{Packet, PacketFramer, crc32_mpeg2},
|
||||
};
|
||||
|
||||
const PID_SDT: u16 = 0x0011;
|
||||
const PID_NIT: u16 = 0x0010;
|
||||
const TABLE_ID_NIT_ACTUAL: u8 = 0x40;
|
||||
const TABLE_ID_SDT_ACTUAL: u8 = 0x42;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ServiceCollector {
|
||||
channel: ConfigChannel,
|
||||
framer: PacketFramer,
|
||||
assembler: SectionAssembler,
|
||||
sdt_assembler: SectionAssembler,
|
||||
nit_assembler: SectionAssembler,
|
||||
services: HashMap<u64, Service>,
|
||||
sections_seen: HashSet<(u8, u8)>,
|
||||
last_section: Option<u8>,
|
||||
network: Option<(u16, Option<u8>)>,
|
||||
packet_count: u64,
|
||||
}
|
||||
|
||||
@@ -33,10 +37,12 @@ impl ServiceCollector {
|
||||
Self {
|
||||
channel,
|
||||
framer: PacketFramer::default(),
|
||||
assembler: SectionAssembler::default(),
|
||||
sdt_assembler: SectionAssembler::default(),
|
||||
nit_assembler: SectionAssembler::default(),
|
||||
services: HashMap::new(),
|
||||
sections_seen: HashSet::new(),
|
||||
last_section: None,
|
||||
network: None,
|
||||
packet_count: 0,
|
||||
}
|
||||
}
|
||||
@@ -49,32 +55,42 @@ impl ServiceCollector {
|
||||
let Ok(packet) = Packet::new(&packet_bytes) else {
|
||||
continue;
|
||||
};
|
||||
if packet.pid() != PID_SDT {
|
||||
continue;
|
||||
}
|
||||
for section in self.assembler.push(packet) {
|
||||
let Some(parsed) = parse_sdt_section(§ion, &self.channel) else {
|
||||
continue;
|
||||
};
|
||||
self.last_section = Some(parsed.last_section);
|
||||
self.sections_seen
|
||||
.insert((parsed.version, parsed.section_number));
|
||||
for service in parsed.services {
|
||||
self.services.insert(service.id, service);
|
||||
match packet.pid() {
|
||||
PID_NIT => {
|
||||
for section in self.nit_assembler.push(packet) {
|
||||
if let Some(network) = parse_nit_section(§ion) {
|
||||
self.network = Some(network);
|
||||
}
|
||||
}
|
||||
}
|
||||
PID_SDT => {
|
||||
for section in self.sdt_assembler.push(packet) {
|
||||
let Some(parsed) = parse_sdt_section(§ion, &self.channel) else {
|
||||
continue;
|
||||
};
|
||||
self.last_section = Some(parsed.last_section);
|
||||
self.sections_seen
|
||||
.insert((parsed.version, parsed.section_number));
|
||||
for service in parsed.services {
|
||||
self.services.insert(service.id, service);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_complete(&self) -> bool {
|
||||
self.last_section.is_some_and(|last| {
|
||||
(0..=last).all(|section| {
|
||||
self.sections_seen
|
||||
.iter()
|
||||
.any(|(_, seen_section)| *seen_section == section)
|
||||
self.network.is_some()
|
||||
&& self.last_section.is_some_and(|last| {
|
||||
(0..=last).all(|section| {
|
||||
self.sections_seen
|
||||
.iter()
|
||||
.any(|(_, seen_section)| *seen_section == section)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
@@ -83,13 +99,77 @@ impl ServiceCollector {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn into_services(self) -> Vec<Service> {
|
||||
pub fn into_services(mut self) -> Vec<Service> {
|
||||
if let Some((network_id, Some(remote_control_key_id))) = self.network {
|
||||
for service in self.services.values_mut() {
|
||||
if service.network_id == network_id {
|
||||
service.remote_control_key_id = Some(remote_control_key_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut services = self.services.into_values().collect::<Vec<_>>();
|
||||
services.sort_by_key(|service| service.service_id);
|
||||
services
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_nit_section(section: &[u8]) -> Option<(u16, Option<u8>)> {
|
||||
if section.len() < 16
|
||||
|| section[0] != TABLE_ID_NIT_ACTUAL
|
||||
|| section[5] & 0x01 == 0
|
||||
|| crc32_mpeg2(section) != 0
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let network_id = u16::from_be_bytes([section[3], section[4]]);
|
||||
let end = section.len().checked_sub(4)?;
|
||||
let network_descriptors_length =
|
||||
(usize::from(section[8] & 0x0f) << 8) | usize::from(section[9]);
|
||||
let transport_length_offset = 10usize.checked_add(network_descriptors_length)?;
|
||||
if transport_length_offset + 2 > end {
|
||||
return None;
|
||||
}
|
||||
let transport_loop_length = (usize::from(section[transport_length_offset] & 0x0f) << 8)
|
||||
| usize::from(section[transport_length_offset + 1]);
|
||||
let mut offset = transport_length_offset + 2;
|
||||
let transport_end = offset.checked_add(transport_loop_length)?;
|
||||
if transport_end > end {
|
||||
return None;
|
||||
}
|
||||
let mut remote_control_key_id = None;
|
||||
while offset + 6 <= transport_end {
|
||||
let descriptors_length =
|
||||
(usize::from(section[offset + 4] & 0x0f) << 8) | usize::from(section[offset + 5]);
|
||||
let descriptors_start = offset + 6;
|
||||
let descriptors_end = descriptors_start.checked_add(descriptors_length)?;
|
||||
if descriptors_end > transport_end {
|
||||
return None;
|
||||
}
|
||||
remote_control_key_id =
|
||||
parse_remote_control_key_id(§ion[descriptors_start..descriptors_end]);
|
||||
if remote_control_key_id.is_some() {
|
||||
break;
|
||||
}
|
||||
offset = descriptors_end;
|
||||
}
|
||||
Some((network_id, remote_control_key_id))
|
||||
}
|
||||
|
||||
fn parse_remote_control_key_id(mut descriptors: &[u8]) -> Option<u8> {
|
||||
while descriptors.len() >= 2 {
|
||||
let length = usize::from(descriptors[1]);
|
||||
let end = 2usize.checked_add(length)?;
|
||||
if end > descriptors.len() {
|
||||
return None;
|
||||
}
|
||||
if descriptors[0] == 0xcd && length >= 1 {
|
||||
return Some(descriptors[2]);
|
||||
}
|
||||
descriptors = &descriptors[end..];
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
struct ParsedSdt {
|
||||
version: u8,
|
||||
section_number: u8,
|
||||
@@ -122,7 +202,7 @@ fn parse_sdt_section(section: &[u8], channel: &ConfigChannel) -> Option<ParsedSd
|
||||
.service_id
|
||||
.is_none_or(|configured| configured == service_id)
|
||||
{
|
||||
if let Some((service_type, name)) =
|
||||
if let Some((service_type, name, logo_id)) =
|
||||
parse_service_descriptor(§ion[descriptor_start..descriptor_end])
|
||||
{
|
||||
services.push(Service {
|
||||
@@ -131,7 +211,7 @@ fn parse_sdt_section(section: &[u8], channel: &ConfigChannel) -> Option<ParsedSd
|
||||
network_id,
|
||||
name,
|
||||
service_type,
|
||||
logo_id: None,
|
||||
logo_id,
|
||||
has_logo_data: None,
|
||||
remote_control_key_id: None,
|
||||
epg_ready: None,
|
||||
@@ -155,34 +235,42 @@ fn parse_sdt_section(section: &[u8], channel: &ConfigChannel) -> Option<ParsedSd
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_service_descriptor(mut descriptors: &[u8]) -> Option<(u8, String)> {
|
||||
fn parse_service_descriptor(mut descriptors: &[u8]) -> Option<(u8, String, Option<u16>)> {
|
||||
let mut service = None;
|
||||
let mut logo_id = None;
|
||||
while descriptors.len() >= 2 {
|
||||
let length = usize::from(descriptors[1]);
|
||||
let end = 2usize.checked_add(length)?;
|
||||
if end > descriptors.len() {
|
||||
return None;
|
||||
}
|
||||
if descriptors[0] == 0x48 {
|
||||
let body = &descriptors[2..end];
|
||||
if body.len() < 3 {
|
||||
return None;
|
||||
let body = &descriptors[2..end];
|
||||
match descriptors[0] {
|
||||
0x48 => {
|
||||
if body.len() < 3 {
|
||||
return None;
|
||||
}
|
||||
let provider_length = usize::from(body[1]);
|
||||
let name_length_offset = 2usize.checked_add(provider_length)?;
|
||||
if name_length_offset >= body.len() {
|
||||
return None;
|
||||
}
|
||||
let name_length = usize::from(body[name_length_offset]);
|
||||
let name_start = name_length_offset + 1;
|
||||
let name_end = name_start.checked_add(name_length)?;
|
||||
if name_end > body.len() {
|
||||
return None;
|
||||
}
|
||||
service = Some((body[0], decode_arib(&body[name_start..name_end])));
|
||||
}
|
||||
let provider_length = usize::from(body[1]);
|
||||
let name_length_offset = 2usize.checked_add(provider_length)?;
|
||||
if name_length_offset >= body.len() {
|
||||
return None;
|
||||
0xcf if body.len() >= 3 && matches!(body[0], 1 | 2) => {
|
||||
logo_id = Some((u16::from(body[1] & 0x01) << 8) | u16::from(body[2]));
|
||||
}
|
||||
let name_length = usize::from(body[name_length_offset]);
|
||||
let name_start = name_length_offset + 1;
|
||||
let name_end = name_start.checked_add(name_length)?;
|
||||
if name_end > body.len() {
|
||||
return None;
|
||||
}
|
||||
return Some((body[0], decode_arib(&body[name_start..name_end])));
|
||||
_ => {}
|
||||
}
|
||||
descriptors = &descriptors[end..];
|
||||
}
|
||||
None
|
||||
service.map(|(service_type, name)| (service_type, name, logo_id))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -193,29 +281,45 @@ mod tests {
|
||||
|
||||
use super::ServiceCollector;
|
||||
|
||||
#[test]
|
||||
fn discovers_service_from_sdt() {
|
||||
let descriptor = [
|
||||
0x48, 0x0c, 0x01, 0x00, 0x09, 0x0e, b'T', b'e', b's', b't', b' ', b'T', b'V', b'!',
|
||||
];
|
||||
let mut section = vec![
|
||||
0x42, 0xf0, 0x00, 0x00, 0x01, 0xc1, 0x00, 0x00, 0x7f, 0xf0, 0xff, 0x00, 0x65, 0xfc,
|
||||
0x80, 0x0e,
|
||||
];
|
||||
section.extend_from_slice(&descriptor);
|
||||
fn section_packet(pid: u16, section: &[u8]) -> [u8; PACKET_SIZE] {
|
||||
let mut packet = [0xff; PACKET_SIZE];
|
||||
packet[0] = 0x47;
|
||||
packet[1] = 0x40 | u8::try_from(pid >> 8).expect("PID");
|
||||
packet[2] = u8::try_from(pid & 0xff).expect("PID");
|
||||
packet[3] = 0x10;
|
||||
packet[4] = 0;
|
||||
packet[5..5 + section.len()].copy_from_slice(section);
|
||||
packet
|
||||
}
|
||||
|
||||
fn finish_section(mut section: Vec<u8>) -> Vec<u8> {
|
||||
let section_length = section.len() - 3 + 4;
|
||||
section[1] = 0xf0 | u8::try_from(section_length >> 8).expect("section length");
|
||||
section[2] = u8::try_from(section_length & 0xff).expect("section length");
|
||||
let crc = crc32_mpeg2(§ion);
|
||||
section.extend_from_slice(&crc.to_be_bytes());
|
||||
section
|
||||
}
|
||||
|
||||
let mut packet = [0xff; PACKET_SIZE];
|
||||
packet[0] = 0x47;
|
||||
packet[1] = 0x40;
|
||||
packet[2] = 0x11;
|
||||
packet[3] = 0x10;
|
||||
packet[4] = 0;
|
||||
packet[5..5 + section.len()].copy_from_slice(§ion);
|
||||
#[test]
|
||||
fn discovers_service_from_sdt() {
|
||||
let descriptor = [
|
||||
0x48, 0x0c, 0x01, 0x00, 0x09, 0x0e, b'T', b'e', b's', b't', b' ', b'T', b'V', b'!',
|
||||
0xcf, 0x03, 0x02, 0x00, 0x07,
|
||||
];
|
||||
let section = finish_section(
|
||||
vec![
|
||||
0x42, 0xf0, 0x00, 0x00, 0x01, 0xc1, 0x00, 0x00, 0x7f, 0xf0, 0xff, 0x00, 0x65, 0xfc,
|
||||
0x80, 0x13,
|
||||
]
|
||||
.into_iter()
|
||||
.chain(descriptor)
|
||||
.collect(),
|
||||
);
|
||||
let nit = finish_section(vec![
|
||||
0x40, 0xf0, 0x00, 0x7f, 0xf0, 0xc1, 0x00, 0x00, 0xf0, 0x00, 0xf0, 0x09, 0x00, 0x01,
|
||||
0x7f, 0xf0, 0xf0, 0x03, 0xcd, 0x01, 0x05,
|
||||
]);
|
||||
|
||||
let mut collector = ServiceCollector::new(ConfigChannel {
|
||||
name: "channel".into(),
|
||||
@@ -231,12 +335,15 @@ mod tests {
|
||||
freq: None,
|
||||
polarity: None,
|
||||
});
|
||||
collector.push(&packet);
|
||||
collector.push(§ion_packet(0x10, &nit));
|
||||
collector.push(§ion_packet(0x11, §ion));
|
||||
assert!(collector.is_complete());
|
||||
let services = collector.into_services();
|
||||
assert_eq!(services.len(), 1);
|
||||
assert_eq!(services[0].name, "Test TV!");
|
||||
assert_eq!(services[0].network_id, 0x7ff0);
|
||||
assert_eq!(services[0].service_id, 101);
|
||||
assert_eq!(services[0].logo_id, Some(7));
|
||||
assert_eq!(services[0].remote_control_key_id, Some(5));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,23 +68,31 @@ impl Drop for TunerSubscription {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TunerManager {
|
||||
devices: Arc<Vec<DeviceHandle>>,
|
||||
respawn_count: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl TunerManager {
|
||||
#[must_use]
|
||||
pub fn new(configs: &[ConfigTuner]) -> Self {
|
||||
let respawn_count = Arc::new(AtomicU64::new(0));
|
||||
let devices = configs
|
||||
.iter()
|
||||
.filter(|config| config.is_disabled != Some(true))
|
||||
.cloned()
|
||||
.enumerate()
|
||||
.map(|(index, config)| DeviceHandle::spawn(index, config))
|
||||
.map(|(index, config)| DeviceHandle::spawn(index, config, respawn_count.clone()))
|
||||
.collect();
|
||||
Self {
|
||||
devices: Arc::new(devices),
|
||||
respawn_count,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn respawn_count(&self) -> u64 {
|
||||
self.respawn_count.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Subscribes to a compatible tuner, sharing an existing multiplex when possible.
|
||||
///
|
||||
/// # Errors
|
||||
@@ -173,11 +181,11 @@ struct DeviceHandle {
|
||||
}
|
||||
|
||||
impl DeviceHandle {
|
||||
fn spawn(index: usize, config: ConfigTuner) -> Self {
|
||||
fn spawn(index: usize, config: ConfigTuner, respawn_count: Arc<AtomicU64>) -> Self {
|
||||
let (command, receiver) = mpsc::channel(DEVICE_COMMAND_CAPACITY);
|
||||
let actor_command = command.clone();
|
||||
tokio::spawn(async move {
|
||||
DeviceActor::new(index, config, actor_command)
|
||||
DeviceActor::new(index, config, actor_command, respawn_count)
|
||||
.run(receiver)
|
||||
.await;
|
||||
});
|
||||
@@ -265,10 +273,16 @@ struct DeviceActor {
|
||||
fault: bool,
|
||||
fatal_count: u8,
|
||||
generation: u64,
|
||||
respawn_count: Arc<AtomicU64>,
|
||||
}
|
||||
|
||||
impl DeviceActor {
|
||||
fn new(index: usize, config: ConfigTuner, command_sender: mpsc::Sender<DeviceCommand>) -> Self {
|
||||
fn new(
|
||||
index: usize,
|
||||
config: ConfigTuner,
|
||||
command_sender: mpsc::Sender<DeviceCommand>,
|
||||
respawn_count: Arc<AtomicU64>,
|
||||
) -> Self {
|
||||
Self {
|
||||
index,
|
||||
config,
|
||||
@@ -282,6 +296,7 @@ impl DeviceActor {
|
||||
fault: false,
|
||||
fatal_count: 0,
|
||||
generation: 0,
|
||||
respawn_count,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -461,6 +476,7 @@ impl DeviceActor {
|
||||
self.release();
|
||||
return;
|
||||
};
|
||||
self.respawn_count.fetch_add(1, Ordering::Relaxed);
|
||||
tracing::warn!(device = self.index, "respawning tuner after unexpected EOF");
|
||||
if let Err(error) = self.start_process(channel).await {
|
||||
tracing::error!(device = self.index, %error, "failed to respawn tuner");
|
||||
|
||||
Reference in New Issue
Block a user