init
This commit is contained in:
commit
e606448b0a
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
204
Cargo.lock
generated
Normal file
204
Cargo.lock
generated
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 4
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler2"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crc32fast"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "csv"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938"
|
||||||
|
dependencies = [
|
||||||
|
"csv-core",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "csv-core"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_rs"
|
||||||
|
version = "0.8.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flate2"
|
||||||
|
version = "1.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb"
|
||||||
|
dependencies = [
|
||||||
|
"crc32fast",
|
||||||
|
"miniz_oxide",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||||
|
dependencies = [
|
||||||
|
"adler2",
|
||||||
|
"simd-adler32",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "8.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.103"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.42"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rbufr"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"csv",
|
||||||
|
"encoding_rs",
|
||||||
|
"flate2",
|
||||||
|
"nom",
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.20"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.228"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simd-adler32"
|
||||||
|
version = "0.3.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.111"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "2.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "2.0.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||||
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "rbufr"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "librbufr"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
csv = "1.4.0"
|
||||||
|
encoding_rs = "0.8.35"
|
||||||
|
flate2 = "1.1.5"
|
||||||
|
nom = "8.0.0"
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
thiserror = "2.0.17"
|
||||||
77
src/block.rs
Normal file
77
src/block.rs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
use crate::errors::Result;
|
||||||
|
use crate::{discriptor_table::*, structs::versions::BUFRMessage};
|
||||||
|
|
||||||
|
pub struct MessageBlock {
|
||||||
|
message: BUFRMessage,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for MessageBlock {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageBlock {
|
||||||
|
pub fn new(message: BUFRMessage) -> Self {
|
||||||
|
MessageBlock { message }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_data(&self) -> Result<()> {
|
||||||
|
let table_info = self.message.table_info();
|
||||||
|
|
||||||
|
let mut b_table_loader = TableLoader::<BTable>::new();
|
||||||
|
let master_b_sequences =
|
||||||
|
b_table_loader.load_table(TT::Standard, table_info.master_table_version)?;
|
||||||
|
|
||||||
|
let mut d_table_loader = TableLoader::<DTable>::new();
|
||||||
|
let master_d_sequences =
|
||||||
|
d_table_loader.load_table(TT::Standard, table_info.master_table_version)?;
|
||||||
|
|
||||||
|
let local_table_version = table_info.local_table_version as u32;
|
||||||
|
|
||||||
|
if local_table_version > 0 {
|
||||||
|
let local_b_sequences = b_table_loader.load_table(
|
||||||
|
TT::Localized(local_table_version),
|
||||||
|
table_info.local_table_version,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let local_d_sequences = d_table_loader.load_table(
|
||||||
|
TT::Localized(local_table_version),
|
||||||
|
table_info.local_table_version,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
// master_b_table.load_table(TT::Standard);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BUFRFile {
|
||||||
|
messages: Vec<MessageBlock>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BUFRFile {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
BUFRFile {
|
||||||
|
messages: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn push_message(&mut self, message: BUFRMessage) {
|
||||||
|
self.messages.push(MessageBlock::new(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the number of successfully parsed messages
|
||||||
|
pub fn message_count(&self) -> usize {
|
||||||
|
self.messages.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn message_at(&self, index: usize) -> Option<&MessageBlock> {
|
||||||
|
self.messages.get(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a reference to all parsed messages
|
||||||
|
pub fn messages(&self) -> &[MessageBlock] {
|
||||||
|
&self.messages
|
||||||
|
}
|
||||||
|
}
|
||||||
162
src/discriptor_table.rs
Normal file
162
src/discriptor_table.rs
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
use crate::errors::Result;
|
||||||
|
use encoding_rs::WINDOWS_1252;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
mod btable;
|
||||||
|
mod dtable;
|
||||||
|
|
||||||
|
pub use btable::BTable;
|
||||||
|
pub use dtable::DTable;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum TableType {
|
||||||
|
A,
|
||||||
|
B,
|
||||||
|
C,
|
||||||
|
D,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BUFRTable;
|
||||||
|
pub trait TableTrait {
|
||||||
|
fn file_path(table_type: TableType, sub_center: Option<u32>, table_version: u8) -> PathBuf;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BUFRTable {
|
||||||
|
pub fn file_path(table_type: TableType, table_version: u8) -> PathBuf {
|
||||||
|
let base_dir = Path::new("tables/bufr");
|
||||||
|
let file_name = match table_type {
|
||||||
|
TableType::A => format!("bufrtaba_{}.csv", table_version),
|
||||||
|
TableType::B => format!("bufrtabb_{}.csv", table_version),
|
||||||
|
TableType::C => format!("bufrtabc_{}.csv", table_version),
|
||||||
|
TableType::D => format!("bufrtabd_{}.csv", table_version),
|
||||||
|
};
|
||||||
|
base_dir.join(file_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LocalTable;
|
||||||
|
|
||||||
|
impl LocalTable {
|
||||||
|
pub fn file_path(table_type: TableType, sub_center: u32, table_version: u8) -> PathBuf {
|
||||||
|
let base_dir = Path::new("tables/local");
|
||||||
|
let file_name = match table_type {
|
||||||
|
TableType::A => format!("loctaba_{}_{}.csv", sub_center * 256, table_version),
|
||||||
|
TableType::B => format!("loctabb_{}_{}.csv", sub_center * 256, table_version),
|
||||||
|
TableType::C => format!("loctabc_{}_{}.csv", sub_center * 256, table_version),
|
||||||
|
TableType::D => format!("loctabd_{}_{}.csv", sub_center * 256, table_version),
|
||||||
|
};
|
||||||
|
base_dir.join(file_name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableTrait for BUFRTable {
|
||||||
|
fn file_path(table_type: TableType, sub_center: Option<u32>, table_version: u8) -> PathBuf {
|
||||||
|
BUFRTable::file_path(table_type, table_version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableTrait for LocalTable {
|
||||||
|
fn file_path(table_type: TableType, sub_center: Option<u32>, table_version: u8) -> PathBuf {
|
||||||
|
let sub_center = sub_center.expect("Sub-center must be provided for LocalTable");
|
||||||
|
LocalTable::file_path(table_type, sub_center, table_version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TableLoader<T: TableT> {
|
||||||
|
sequences: Vec<T::Seq>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum TT {
|
||||||
|
Localized(u32), // sub_center
|
||||||
|
Standard,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: TableT> TableLoader<T> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
sequences: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_table(&mut self, table_type: TT, table_version: u8) -> Result<Vec<T::Seq>> {
|
||||||
|
let table_kind = T::table_type();
|
||||||
|
|
||||||
|
let local_table_path = match table_type {
|
||||||
|
TT::Localized(sc) => LocalTable::file_path(table_kind, sc, table_version),
|
||||||
|
TT::Standard => BUFRTable::file_path(table_kind, table_version),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Here you would add code to actually load and parse the table from the file at `path`.
|
||||||
|
println!("Loading table from path: {:?}", local_table_path);
|
||||||
|
let raw = fs::read(&local_table_path)?;
|
||||||
|
|
||||||
|
let fixed = normalize_dashes(raw);
|
||||||
|
let text = decode_tabd_text(fixed);
|
||||||
|
|
||||||
|
let mut table = T::default();
|
||||||
|
|
||||||
|
for line in text.lines() {
|
||||||
|
if let Some(seq) = table.parse_line(line) {
|
||||||
|
self.sequences.push(seq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(seq) = table.finish() {
|
||||||
|
self.sequences.push(seq);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut sequences = Vec::new();
|
||||||
|
std::mem::swap(&mut sequences, &mut self.sequences);
|
||||||
|
|
||||||
|
Ok(sequences)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_dashes(mut bytes: Vec<u8>) -> Vec<u8> {
|
||||||
|
for b in &mut bytes {
|
||||||
|
match *b {
|
||||||
|
0x96 | 0x97 => *b = b'-', // EN / EM dash → '-'
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_tabd_text(bytes: Vec<u8>) -> String {
|
||||||
|
let (text, _, _) = WINDOWS_1252.decode(&bytes);
|
||||||
|
text.into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TableT: Default {
|
||||||
|
type Seq;
|
||||||
|
|
||||||
|
fn table_type() -> TableType;
|
||||||
|
fn parse_line(&mut self, line: &str) -> Option<Self::Seq>;
|
||||||
|
fn finish(&mut self) -> Option<Self::Seq> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Descriptor {
|
||||||
|
pub f: i32,
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::discriptor_table::{TableLoader, btable::BTable, dtable::DTable};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_read_table() {
|
||||||
|
// let mut bufr_table: super::Table<super::BUFRTable> = super::Table::new();
|
||||||
|
let mut bufr_table = TableLoader::<BTable>::new();
|
||||||
|
bufr_table.load_table(super::TT::Standard, 11).unwrap();
|
||||||
|
|
||||||
|
println!("{:#?}", bufr_table.sequences);
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/discriptor_table/btable.rs
Normal file
51
src/discriptor_table/btable.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
use crate::discriptor_table::{Descriptor, TableT};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct BTable;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BSeq {
|
||||||
|
pub d: Descriptor,
|
||||||
|
pub scale: i32,
|
||||||
|
pub dw: i32,
|
||||||
|
pub refval: f32,
|
||||||
|
pub unit: String,
|
||||||
|
pub elname: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableT for BTable {
|
||||||
|
type Seq = BSeq;
|
||||||
|
fn table_type() -> super::TableType {
|
||||||
|
super::TableType::B
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_line(&mut self, line: &str) -> Option<Self::Seq> {
|
||||||
|
let fields: Vec<&str> = line.split(';').collect();
|
||||||
|
if fields.len() < 8 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parse_i = |s: &str| s.parse::<i32>().ok();
|
||||||
|
let parse_f = |s: &str| s.parse::<f32>().ok();
|
||||||
|
|
||||||
|
let f = parse_i(fields[0])?;
|
||||||
|
let x = parse_i(fields[1])?;
|
||||||
|
let y = parse_i(fields[2])?;
|
||||||
|
|
||||||
|
let name = fields[3];
|
||||||
|
let unit = fields[4];
|
||||||
|
|
||||||
|
let scale = parse_i(fields[5])?;
|
||||||
|
let refval = parse_f(fields[6])?;
|
||||||
|
let dw = parse_i(fields[7])?;
|
||||||
|
|
||||||
|
Some(BSeq {
|
||||||
|
d: Descriptor { f, x, y },
|
||||||
|
scale,
|
||||||
|
dw,
|
||||||
|
refval,
|
||||||
|
unit: unit.to_string(),
|
||||||
|
elname: name.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/discriptor_table/dtable.rs
Normal file
78
src/discriptor_table/dtable.rs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
use crate::discriptor_table::{Descriptor, TableT, TableType};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DD {
|
||||||
|
pub f: i32,
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct DSeq {
|
||||||
|
pub d: Descriptor,
|
||||||
|
pub del: Vec<DD>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct DTable {
|
||||||
|
current_seq: Option<DSeq>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableT for DTable {
|
||||||
|
type Seq = DSeq;
|
||||||
|
|
||||||
|
fn table_type() -> TableType {
|
||||||
|
TableType::D
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_line(&mut self, line: &str) -> Option<Self::Seq> {
|
||||||
|
let fields: Vec<&str> = line.split(';').collect();
|
||||||
|
|
||||||
|
if fields.len() < 6 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parse_i = |s: &str| s.trim().parse::<i32>().unwrap_or(0);
|
||||||
|
|
||||||
|
let isf = parse_i(fields[0]);
|
||||||
|
let isx = parse_i(fields[1]);
|
||||||
|
let isy = parse_i(fields[2]);
|
||||||
|
let idf = parse_i(fields[3]);
|
||||||
|
let idx = parse_i(fields[4]);
|
||||||
|
let idy = parse_i(fields[5]);
|
||||||
|
|
||||||
|
let mut finished_seq = None;
|
||||||
|
let current = &mut self.current_seq;
|
||||||
|
|
||||||
|
if isf == 3 || isx != 0 || isy != 0 {
|
||||||
|
if let Some(prev) = current.take() {
|
||||||
|
finished_seq = Some(prev);
|
||||||
|
}
|
||||||
|
|
||||||
|
*current = Some(DSeq {
|
||||||
|
d: Descriptor {
|
||||||
|
f: isf,
|
||||||
|
x: isx,
|
||||||
|
y: isy,
|
||||||
|
},
|
||||||
|
del: Vec::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if idf != 0 || idx != 0 || idy != 0 {
|
||||||
|
if let Some(seq) = current.as_mut() {
|
||||||
|
seq.del.push(DD {
|
||||||
|
f: idf,
|
||||||
|
x: idx,
|
||||||
|
y: idy,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finished_seq
|
||||||
|
}
|
||||||
|
|
||||||
|
fn finish(&mut self) -> Option<Self::Seq> {
|
||||||
|
self.current_seq.take()
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/errors.rs
Normal file
31
src/errors.rs
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
use nom;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("IO Error: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error("CSV Error: {0}")]
|
||||||
|
Csv(#[from] csv::Error),
|
||||||
|
|
||||||
|
#[error("Table not found for type {0:?}, sub_center {1:?}, version {2}")]
|
||||||
|
TableNotFound(crate::discriptor_table::TableType, Option<u32>, u8),
|
||||||
|
|
||||||
|
#[error("Parse Error: {0}")]
|
||||||
|
ParseError(String),
|
||||||
|
|
||||||
|
#[error("File is not a valid BUFR file")]
|
||||||
|
Nom(String),
|
||||||
|
|
||||||
|
#[error("Unsupported BUFR version: {0}")]
|
||||||
|
UnsupportedVersion(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<nom::Err<nom::error::Error<&'a [u8]>>> for Error {
|
||||||
|
fn from(value: nom::Err<nom::error::Error<&'a [u8]>>) -> Self {
|
||||||
|
Self::Nom(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
5
src/lib.rs
Normal file
5
src/lib.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
mod block;
|
||||||
|
mod discriptor_table;
|
||||||
|
mod errors;
|
||||||
|
pub mod parser;
|
||||||
|
pub mod structs;
|
||||||
3
src/main.rs
Normal file
3
src/main.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
println!("Hello, world!");
|
||||||
|
}
|
||||||
165
src/parser.rs
Normal file
165
src/parser.rs
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
use crate::block::{BUFRFile, MessageBlock};
|
||||||
|
use crate::errors::Result;
|
||||||
|
use crate::structs::versions::BUFRMessage;
|
||||||
|
use flate2::read::GzDecoder;
|
||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::{BufReader, Cursor, Read, Seek, SeekFrom},
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
|
||||||
|
const BUFR_PATTERN: &[u8] = b"BUFR";
|
||||||
|
const BUFFER_SIZE: usize = 8192; // 8KB buffer for scanning
|
||||||
|
// const MAX_MESSAGE_SIZE: usize = 500_000; // 500KB max message size
|
||||||
|
|
||||||
|
pub struct Parser;
|
||||||
|
|
||||||
|
impl Parser {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find all offsets in the file where "BUFR" appears using streaming approach
|
||||||
|
fn find_bufr_offsets<R: Read + Seek>(reader: &mut R) -> Result<Vec<u64>> {
|
||||||
|
let mut offsets = Vec::new();
|
||||||
|
let mut buffer = vec![0u8; BUFFER_SIZE];
|
||||||
|
let mut file_offset = 0u64;
|
||||||
|
let mut overlap = vec![0u8; BUFR_PATTERN.len() - 1];
|
||||||
|
let mut overlap_len = 0;
|
||||||
|
|
||||||
|
reader.seek(SeekFrom::Start(0))?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let bytes_read = reader.read(&mut buffer)?;
|
||||||
|
if bytes_read == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a combined view of overlap + new data
|
||||||
|
let mut search_buffer = Vec::with_capacity(overlap_len + bytes_read);
|
||||||
|
search_buffer.extend_from_slice(&overlap[..overlap_len]);
|
||||||
|
search_buffer.extend_from_slice(&buffer[..bytes_read]);
|
||||||
|
|
||||||
|
// Search for BUFR pattern
|
||||||
|
for i in 0..search_buffer.len().saturating_sub(BUFR_PATTERN.len() - 1) {
|
||||||
|
if search_buffer.len() >= i + BUFR_PATTERN.len()
|
||||||
|
&& &search_buffer[i..i + BUFR_PATTERN.len()] == BUFR_PATTERN
|
||||||
|
{
|
||||||
|
let actual_offset = file_offset - overlap_len as u64 + i as u64;
|
||||||
|
offsets.push(actual_offset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save overlap for next iteration
|
||||||
|
if bytes_read >= BUFR_PATTERN.len() - 1 {
|
||||||
|
overlap_len = BUFR_PATTERN.len() - 1;
|
||||||
|
overlap[..overlap_len]
|
||||||
|
.copy_from_slice(&buffer[bytes_read - overlap_len..bytes_read]);
|
||||||
|
} else {
|
||||||
|
overlap_len = bytes_read;
|
||||||
|
overlap[..overlap_len].copy_from_slice(&buffer[..bytes_read]);
|
||||||
|
}
|
||||||
|
|
||||||
|
file_offset += bytes_read as u64;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(offsets)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a BUFR message from file at specific offset
|
||||||
|
fn read_message_at_offset<R: Read + Seek>(reader: &mut R, offset: u64) -> Result<Vec<u8>> {
|
||||||
|
reader.seek(SeekFrom::Start(offset))?;
|
||||||
|
|
||||||
|
// Read Section 0 to get total length
|
||||||
|
let mut section0_buf = [0u8; 8];
|
||||||
|
reader.read_exact(&mut section0_buf)?;
|
||||||
|
|
||||||
|
// Parse total length (3 bytes starting at offset 4)
|
||||||
|
let total_length =
|
||||||
|
u32::from_be_bytes([0, section0_buf[4], section0_buf[5], section0_buf[6]]);
|
||||||
|
|
||||||
|
// Limit message size for safety
|
||||||
|
// if total_length as usize > MAX_MESSAGE_SIZE {
|
||||||
|
// return Err(crate::errors::Error::ParseError(format!(
|
||||||
|
// "Message too large: {} bytes",
|
||||||
|
// total_length
|
||||||
|
// )));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Read entire message
|
||||||
|
let mut message_buf = vec![0u8; total_length as usize];
|
||||||
|
reader.seek(SeekFrom::Start(offset))?;
|
||||||
|
reader.read_exact(&mut message_buf)?;
|
||||||
|
|
||||||
|
Ok(message_buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a file containing one or more BUFR messages using streaming approach
|
||||||
|
pub fn parse<P: AsRef<Path>>(&mut self, path: P) -> Result<BUFRFile> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let mut reader = BufReader::new(file);
|
||||||
|
|
||||||
|
// Try to detect gzip compression
|
||||||
|
let mut magic_bytes = [0u8; 2];
|
||||||
|
reader.read_exact(&mut magic_bytes)?;
|
||||||
|
reader.seek(SeekFrom::Start(0))?;
|
||||||
|
if magic_bytes == [0x1F, 0x8B] {
|
||||||
|
// Gzip magic number detected
|
||||||
|
let mut gz_decoder = GzDecoder::new(reader);
|
||||||
|
let mut bytes = vec![];
|
||||||
|
gz_decoder.read_to_end(&mut bytes)?;
|
||||||
|
|
||||||
|
self.parse_inner(&mut Cursor::new(bytes))
|
||||||
|
} else {
|
||||||
|
// Not compressed
|
||||||
|
// Rewind reader
|
||||||
|
reader.seek(SeekFrom::Start(0))?;
|
||||||
|
self.parse_inner(&mut reader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_inner<R>(&self, buf_reader: &mut R) -> Result<BUFRFile>
|
||||||
|
where
|
||||||
|
R: Read + Seek,
|
||||||
|
{
|
||||||
|
// Find all BUFR message offsets
|
||||||
|
let offsets = Self::find_bufr_offsets(buf_reader)?;
|
||||||
|
|
||||||
|
let mut file_block = BUFRFile::new();
|
||||||
|
|
||||||
|
// Parse each BUFR message
|
||||||
|
for offset in offsets {
|
||||||
|
match Self::read_message_at_offset(buf_reader, offset) {
|
||||||
|
Ok(message_data) => match BUFRMessage::parse(&message_data) {
|
||||||
|
Ok((_, message)) => {
|
||||||
|
file_block.push_message(message);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to parse BUFR message at offset {}: {:?}", offset, e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to read BUFR message at offset {}: {:?}", offset, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(file_block)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use super::Parser;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test() {
|
||||||
|
let mut parser = Parser::new();
|
||||||
|
if let Ok(file) = parser.parse("/Users/xiang.li1/Downloads/36_2025-12-22T11_00_00.bufr") {
|
||||||
|
for message in file.messages() {
|
||||||
|
println!("{}", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/structs/bit.rs
Normal file
22
src/structs/bit.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
use nom::IResult;
|
||||||
|
use nom::bits::{bits, bytes, complete::take};
|
||||||
|
|
||||||
|
pub type BitInput<'a> = (&'a [u8], usize);
|
||||||
|
|
||||||
|
pub fn parse_arbitrary_bits(input: BitInput, count: usize) -> IResult<BitInput, u32> {
|
||||||
|
take(count)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::structs::bit::parse_arbitrary_bits;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test() {
|
||||||
|
let data = [0xA0, 0xA0, 0x01, 0xA0];
|
||||||
|
|
||||||
|
let result = parse_arbitrary_bits((&data, 0), 16).unwrap();
|
||||||
|
|
||||||
|
println!("{:?}", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/structs/mod.rs
Normal file
25
src/structs/mod.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
use nom::{
|
||||||
|
IResult,
|
||||||
|
bytes::complete::{tag, take},
|
||||||
|
number::complete::{be_u8, be_u16, be_u24},
|
||||||
|
};
|
||||||
|
pub mod bit;
|
||||||
|
pub mod versions;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn skip(n: usize) -> impl Fn(&[u8]) -> IResult<&[u8], ()> {
|
||||||
|
move |input: &[u8]| {
|
||||||
|
let (input, _) = take(n)(input)?;
|
||||||
|
Ok((input, ()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn skip1(input: &[u8]) -> IResult<&[u8], ()> {
|
||||||
|
skip(1)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn skip2(input: &[u8]) -> IResult<&[u8], ()> {
|
||||||
|
skip(2)(input)
|
||||||
|
}
|
||||||
89
src/structs/versions/mod.rs
Normal file
89
src/structs/versions/mod.rs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
pub mod v2;
|
||||||
|
pub mod v4;
|
||||||
|
pub(super) use super::{skip, skip1, skip2};
|
||||||
|
use crate::errors::{Error, Result};
|
||||||
|
use nom::{
|
||||||
|
IResult,
|
||||||
|
bytes::complete::{tag, take},
|
||||||
|
number::complete::{be_u8, be_u16, be_u24},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum BUFRMessage {
|
||||||
|
V2(v2::BUFRMessageV2),
|
||||||
|
V4(v4::BUFRMessageV4),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BUFRMessage {
|
||||||
|
pub fn parse(input: &[u8]) -> Result<(&[u8], Self)> {
|
||||||
|
let (_, section0) = parse_section0(input)?;
|
||||||
|
match section0.version {
|
||||||
|
2 => {
|
||||||
|
let (input, msg) = v2::BUFRMessageV2::parse(input)?;
|
||||||
|
Ok((input, BUFRMessage::V2(msg)))
|
||||||
|
}
|
||||||
|
4 => {
|
||||||
|
let (input, msg) = v4::BUFRMessageV4::parse(input)?;
|
||||||
|
Ok((input, BUFRMessage::V4(msg)))
|
||||||
|
}
|
||||||
|
_ => Err(Error::UnsupportedVersion(section0.version)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn version(&self) -> u8 {
|
||||||
|
match self {
|
||||||
|
BUFRMessage::V2(_) => 2,
|
||||||
|
BUFRMessage::V4(_) => 4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn table_info(&self) -> TableInfo {
|
||||||
|
match self {
|
||||||
|
BUFRMessage::V2(msg) => msg.table_info(),
|
||||||
|
BUFRMessage::V4(msg) => msg.table_info(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for BUFRMessage {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
BUFRMessage::V2(msg) => msg.description(f),
|
||||||
|
BUFRMessage::V4(msg) => msg.description(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait MessageVersion: Sized {
|
||||||
|
fn parse(input: &[u8]) -> IResult<&[u8], Self>;
|
||||||
|
|
||||||
|
fn description(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
|
||||||
|
|
||||||
|
fn table_info(&self) -> TableInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TableInfo {
|
||||||
|
pub master_table_version: u8,
|
||||||
|
pub local_table_version: u8,
|
||||||
|
pub center_id: u16,
|
||||||
|
pub subcenter_id: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct Section0 {
|
||||||
|
pub total_length: u32,
|
||||||
|
pub version: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_section0(input: &[u8]) -> IResult<&[u8], Section0> {
|
||||||
|
let (input, _) = tag("BUFR")(input)?;
|
||||||
|
let (input, total_length) = be_u24(input)?;
|
||||||
|
let (input, edition) = be_u8(input)?;
|
||||||
|
Ok((
|
||||||
|
input,
|
||||||
|
Section0 {
|
||||||
|
total_length,
|
||||||
|
version: edition,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
281
src/structs/versions/v2.rs
Normal file
281
src/structs/versions/v2.rs
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
use nom::{
|
||||||
|
IResult,
|
||||||
|
bytes::complete::{tag, take},
|
||||||
|
number::complete::{be_u8, be_u16, be_u24},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::structs::versions::MessageVersion;
|
||||||
|
|
||||||
|
use super::skip1;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct BUFRMessageV2 {
|
||||||
|
pub section0: Section0,
|
||||||
|
pub section1: Section1,
|
||||||
|
pub section2: Option<Section2>,
|
||||||
|
pub section3: Section3,
|
||||||
|
pub section4: Section4,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageVersion for BUFRMessageV2 {
|
||||||
|
fn parse(input: &[u8]) -> IResult<&[u8], Self> {
|
||||||
|
let (input, section0) = parse_section0(input)?;
|
||||||
|
let (input, section1) = parse_section1(input)?;
|
||||||
|
let (input, section2) = if section1.optional_section_present {
|
||||||
|
let (input, sec2) = parse_section2(input)?;
|
||||||
|
(input, Some(sec2))
|
||||||
|
} else {
|
||||||
|
(input, None)
|
||||||
|
};
|
||||||
|
let (input, section3) = parse_section3(input)?;
|
||||||
|
let (input, section4) = parse_section4(input)?;
|
||||||
|
let (input, _section5) = parse_section5(input)?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
input,
|
||||||
|
BUFRMessageV2 {
|
||||||
|
section0,
|
||||||
|
section1,
|
||||||
|
section2,
|
||||||
|
section3,
|
||||||
|
section4,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "BUFR Message V2:\n")?;
|
||||||
|
write!(f, "{}\n", self.section1)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn table_info(&self) -> super::TableInfo {
|
||||||
|
super::TableInfo {
|
||||||
|
master_table_version: self.section1.master_table_version,
|
||||||
|
local_table_version: self.section1.local_table_version,
|
||||||
|
center_id: self.section1.centre as u16,
|
||||||
|
subcenter_id: self.section1.subcentre as u16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Section0 {
|
||||||
|
pub total_length: u32,
|
||||||
|
pub version: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_section0(input: &[u8]) -> IResult<&[u8], Section0> {
|
||||||
|
let (input, _) = tag("BUFR")(input)?;
|
||||||
|
let (input, total_length) = be_u24(input)?;
|
||||||
|
let (input, edition) = be_u8(input)?;
|
||||||
|
Ok((
|
||||||
|
input,
|
||||||
|
Section0 {
|
||||||
|
total_length,
|
||||||
|
version: edition,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Section1 {
|
||||||
|
pub length: usize,
|
||||||
|
pub master_table: u8, // octet 4
|
||||||
|
pub subcentre: u8, // octet 5
|
||||||
|
pub centre: u8, // octet 6
|
||||||
|
pub update_sequence_number: u8, // octet 7
|
||||||
|
pub optional_section_present: bool, // octet 8 bit1 (MSB)
|
||||||
|
pub data_category: u8, // octet 9
|
||||||
|
pub data_subcategory: u8, // octet 10
|
||||||
|
pub master_table_version: u8, // octet 11
|
||||||
|
pub local_table_version: u8, // octet 12
|
||||||
|
pub year: u8, // octet 13 (year of century)
|
||||||
|
pub month: u8, // octet 14
|
||||||
|
pub day: u8, // octet 15
|
||||||
|
pub hour: u8, // octet 16
|
||||||
|
pub minute: u8, // octet 17
|
||||||
|
// octet 18- local use: 你可以选择保存或直接跳过
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_section1(input: &[u8]) -> IResult<&[u8], Section1> {
|
||||||
|
let (input, length) = be_u24(input)?;
|
||||||
|
let length = length as usize;
|
||||||
|
|
||||||
|
const FIXED_LEN: usize = 17;
|
||||||
|
if length < FIXED_LEN {
|
||||||
|
return Err(nom::Err::Error(nom::error::Error::new(
|
||||||
|
input,
|
||||||
|
nom::error::ErrorKind::LengthValue,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (input, master_table) = be_u8(input)?;
|
||||||
|
let (input, subcentre) = be_u8(input)?;
|
||||||
|
let (input, centre) = be_u8(input)?;
|
||||||
|
let (input, update_sequence_number) = be_u8(input)?;
|
||||||
|
let (input, optional_section_flag) = be_u8(input)?;
|
||||||
|
let optional_section_present = (optional_section_flag & 0x80) != 0;
|
||||||
|
|
||||||
|
let (input, data_category) = be_u8(input)?;
|
||||||
|
let (input, data_subcategory) = be_u8(input)?;
|
||||||
|
let (input, master_table_version) = be_u8(input)?;
|
||||||
|
let (input, local_table_version) = be_u8(input)?;
|
||||||
|
let (input, year) = be_u8(input)?;
|
||||||
|
let (input, month) = be_u8(input)?;
|
||||||
|
let (input, day) = be_u8(input)?;
|
||||||
|
let (input, hour) = be_u8(input)?;
|
||||||
|
let (input, minute) = be_u8(input)?;
|
||||||
|
|
||||||
|
// 剩余 local-use
|
||||||
|
let local_len = length - FIXED_LEN;
|
||||||
|
let (input, _) = nom::bytes::complete::take(local_len)(input)?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
input,
|
||||||
|
Section1 {
|
||||||
|
length,
|
||||||
|
master_table,
|
||||||
|
subcentre,
|
||||||
|
centre,
|
||||||
|
update_sequence_number,
|
||||||
|
optional_section_present,
|
||||||
|
data_category,
|
||||||
|
data_subcategory,
|
||||||
|
master_table_version,
|
||||||
|
local_table_version,
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day,
|
||||||
|
hour,
|
||||||
|
minute,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Section2 {
|
||||||
|
pub length: usize,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_section2(input: &[u8]) -> IResult<&[u8], Section2> {
|
||||||
|
let (input, length) = be_u24(input)?;
|
||||||
|
let (input, _) = skip1(input)?;
|
||||||
|
let (input, data) = take(length - 4)(input)?;
|
||||||
|
Ok((
|
||||||
|
input,
|
||||||
|
Section2 {
|
||||||
|
length: length as usize,
|
||||||
|
data: data.to_vec(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Section3 {
|
||||||
|
pub length: usize,
|
||||||
|
pub number_of_subsets: u16,
|
||||||
|
pub is_observation: bool,
|
||||||
|
pub is_compressed: bool,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_section3(input: &[u8]) -> IResult<&[u8], Section3> {
|
||||||
|
let (input, length) = be_u24(input)?;
|
||||||
|
let (input, _) = skip1(input)?;
|
||||||
|
let (input, number_of_subsets) = be_u16(input)?;
|
||||||
|
let (input, flags) = be_u8(input)?;
|
||||||
|
let is_observation = (flags & 0b1000_0000) != 0;
|
||||||
|
let is_compressed = (flags & 0b0100_0000) != 0;
|
||||||
|
let (input, data) = take(length - 7)(input)?;
|
||||||
|
Ok((
|
||||||
|
input,
|
||||||
|
Section3 {
|
||||||
|
length: length as usize,
|
||||||
|
number_of_subsets,
|
||||||
|
is_observation,
|
||||||
|
is_compressed,
|
||||||
|
data: data.to_vec(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Section4 {
|
||||||
|
pub length: usize,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_section4(input: &[u8]) -> IResult<&[u8], Section4> {
|
||||||
|
let (input, length) = be_u24(input)?;
|
||||||
|
let (input, _) = skip1(input)?;
|
||||||
|
let (input, data) = take(length - 4)(input)?;
|
||||||
|
Ok((
|
||||||
|
input,
|
||||||
|
Section4 {
|
||||||
|
length: length as usize,
|
||||||
|
data: data.to_vec(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Section1 {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
writeln!(f, "Section 1 (BUFR v2):")?;
|
||||||
|
writeln!(f, " Length: {} bytes", self.length)?;
|
||||||
|
writeln!(f)?;
|
||||||
|
writeln!(f, " Organization:")?;
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
" Centre: {:<5} (0x{:02X})",
|
||||||
|
self.centre, self.centre
|
||||||
|
)?;
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
" Sub-centre: {:<5} (0x{:02X})",
|
||||||
|
self.subcentre, self.subcentre
|
||||||
|
)?;
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
" Update Sequence: {}",
|
||||||
|
self.update_sequence_number
|
||||||
|
)?;
|
||||||
|
writeln!(f)?;
|
||||||
|
writeln!(f, " Data Classification:")?;
|
||||||
|
writeln!(f, " Category: {}", self.data_category)?;
|
||||||
|
writeln!(f, " Sub-category: {}", self.data_subcategory)?;
|
||||||
|
writeln!(f)?;
|
||||||
|
writeln!(f, " Table Versions:")?;
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
" Master Table: {} (v{})",
|
||||||
|
self.master_table, self.master_table_version
|
||||||
|
)?;
|
||||||
|
writeln!(f, " Local Table: v{}", self.local_table_version)?;
|
||||||
|
writeln!(f)?;
|
||||||
|
writeln!(f, " Observation Time:")?;
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
" DateTime: 19{:02}-{:02}-{:02} {:02}:{:02}:00 UTC",
|
||||||
|
self.year, self.month, self.day, self.hour, self.minute
|
||||||
|
)?;
|
||||||
|
writeln!(f)?;
|
||||||
|
writeln!(f, " Optional Data:")?;
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
" Section 2 Present: {}",
|
||||||
|
if self.optional_section_present {
|
||||||
|
"Yes"
|
||||||
|
} else {
|
||||||
|
"No"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Section5;
|
||||||
|
|
||||||
|
fn parse_section5(input: &[u8]) -> IResult<&[u8], Section5> {
|
||||||
|
let (input, _) = tag("7777")(input)?;
|
||||||
|
Ok((input, Section5 {}))
|
||||||
|
}
|
||||||
345
src/structs/versions/v4.rs
Normal file
345
src/structs/versions/v4.rs
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
use crate::structs::versions::MessageVersion;
|
||||||
|
use nom::{
|
||||||
|
IResult,
|
||||||
|
bytes::complete::{tag, take},
|
||||||
|
error::{Error, ErrorKind},
|
||||||
|
number::complete::{be_u8, be_u16, be_u24},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::skip1;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct BUFRMessageV4 {
|
||||||
|
pub section0: Section0,
|
||||||
|
pub section1: Section1,
|
||||||
|
pub section2: Option<Section2>,
|
||||||
|
pub section3: Section3,
|
||||||
|
pub section4: Section4,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageVersion for BUFRMessageV4 {
|
||||||
|
fn parse(input: &[u8]) -> IResult<&[u8], Self> {
|
||||||
|
let (input, section0) = parse_section0(input)?;
|
||||||
|
let (input, section1) = parse_section1(input)?;
|
||||||
|
let (input, section2) = if section1.optional_section_present {
|
||||||
|
let (input, sec2) = parse_section2(input)?;
|
||||||
|
(input, Some(sec2))
|
||||||
|
} else {
|
||||||
|
(input, None)
|
||||||
|
};
|
||||||
|
let (input, section3) = parse_section3(input)?;
|
||||||
|
let (input, section4) = parse_section4(input)?;
|
||||||
|
let (input, _section5) = parse_section5(input)?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
input,
|
||||||
|
BUFRMessageV4 {
|
||||||
|
section0,
|
||||||
|
section1,
|
||||||
|
section2,
|
||||||
|
section3,
|
||||||
|
section4,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn description(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "BUFR Message V4:\n")?;
|
||||||
|
write!(f, "{}\n", self.section1)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn table_info(&self) -> super::TableInfo {
|
||||||
|
super::TableInfo {
|
||||||
|
master_table_version: self.section1.master_table_version,
|
||||||
|
local_table_version: self.section1.local_table_version,
|
||||||
|
center_id: self.section1.centre as u16,
|
||||||
|
subcenter_id: self.section1.subcentre as u16,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Section0 {
|
||||||
|
pub total_length: u32,
|
||||||
|
pub version: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_section0(input: &[u8]) -> IResult<&[u8], Section0> {
|
||||||
|
let (input, _) = tag("BUFR")(input)?;
|
||||||
|
let (input, total_length) = be_u24(input)?;
|
||||||
|
let (input, edition) = be_u8(input)?;
|
||||||
|
Ok((
|
||||||
|
input,
|
||||||
|
Section0 {
|
||||||
|
total_length,
|
||||||
|
version: edition,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Section1 {
|
||||||
|
pub length: usize, // octet 1-3
|
||||||
|
pub master_table: u8, // octet 4
|
||||||
|
pub centre: u16, // octet 5-6
|
||||||
|
pub subcentre: u16, // octet 7-8
|
||||||
|
pub update_sequence_number: u8, // octet 9
|
||||||
|
pub optional_section_present: bool, // octet 10 bit1
|
||||||
|
pub data_category: u8, // octet 11
|
||||||
|
pub international_data_subcategory: u8, // octet 12
|
||||||
|
pub local_subcategory: u8, // octet 13
|
||||||
|
pub master_table_version: u8, // octet 14
|
||||||
|
pub local_table_version: u8, // octet 15
|
||||||
|
pub year: u16, // octet 16-17 (4 digits)
|
||||||
|
pub month: u8, // octet 18
|
||||||
|
pub day: u8, // octet 19
|
||||||
|
pub hour: u8, // octet 20
|
||||||
|
pub minute: u8, // octet 21
|
||||||
|
pub second: u8, // octet 22
|
||||||
|
pub local_use: Vec<u8>, // octet 23-
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_section1(input: &[u8]) -> IResult<&[u8], Section1> {
|
||||||
|
let (input, length_u24) = be_u24(input)?;
|
||||||
|
let length = length_u24 as usize;
|
||||||
|
|
||||||
|
const FIXED_LEN: usize = 22;
|
||||||
|
if length < FIXED_LEN {
|
||||||
|
return Err(nom::Err::Error(Error::new(input, ErrorKind::LengthValue)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let (input, master_table) = be_u8(input)?;
|
||||||
|
let (input, centre) = be_u16(input)?;
|
||||||
|
let (input, subcentre) = be_u16(input)?;
|
||||||
|
let (input, update_sequence_number) = be_u8(input)?;
|
||||||
|
|
||||||
|
let (input, flags) = be_u8(input)?;
|
||||||
|
let optional_section_present = (flags & 0x80) != 0;
|
||||||
|
|
||||||
|
let (input, data_category) = be_u8(input)?;
|
||||||
|
let (input, international_data_subcategory) = be_u8(input)?;
|
||||||
|
let (input, local_subcategory) = be_u8(input)?;
|
||||||
|
let (input, master_table_version) = be_u8(input)?;
|
||||||
|
let (input, local_table_version) = be_u8(input)?;
|
||||||
|
|
||||||
|
let (input, year) = be_u16(input)?;
|
||||||
|
let (input, month) = be_u8(input)?;
|
||||||
|
let (input, day) = be_u8(input)?;
|
||||||
|
let (input, hour) = be_u8(input)?;
|
||||||
|
let (input, minute) = be_u8(input)?;
|
||||||
|
let (input, second) = be_u8(input)?;
|
||||||
|
|
||||||
|
let local_len = length - FIXED_LEN;
|
||||||
|
let (input, local_bytes) = take(local_len)(input)?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
input,
|
||||||
|
Section1 {
|
||||||
|
length,
|
||||||
|
master_table,
|
||||||
|
centre,
|
||||||
|
subcentre,
|
||||||
|
update_sequence_number,
|
||||||
|
optional_section_present,
|
||||||
|
data_category,
|
||||||
|
international_data_subcategory,
|
||||||
|
local_subcategory,
|
||||||
|
master_table_version,
|
||||||
|
local_table_version,
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day,
|
||||||
|
hour,
|
||||||
|
minute,
|
||||||
|
second,
|
||||||
|
local_use: local_bytes.to_vec(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Section1 {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
writeln!(f, "Section 1:")?;
|
||||||
|
writeln!(f, " Length: {} bytes", self.length)?;
|
||||||
|
writeln!(f)?;
|
||||||
|
writeln!(f, " Organization:")?;
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
" Centre: {:<5} (0x{:04X})",
|
||||||
|
self.centre, self.centre
|
||||||
|
)?;
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
" Sub-centre: {:<5} (0x{:04X})",
|
||||||
|
self.subcentre, self.subcentre
|
||||||
|
)?;
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
" Update Sequence: {}",
|
||||||
|
self.update_sequence_number
|
||||||
|
)?;
|
||||||
|
writeln!(f)?;
|
||||||
|
writeln!(f, " Data Classification:")?;
|
||||||
|
writeln!(f, " Category: {}", self.data_category)?;
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
" International Sub: {}",
|
||||||
|
self.international_data_subcategory
|
||||||
|
)?;
|
||||||
|
writeln!(f, " Local Sub: {}", self.local_subcategory)?;
|
||||||
|
writeln!(f)?;
|
||||||
|
writeln!(f, " Table Versions:")?;
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
" Master Table: {} (v{})",
|
||||||
|
self.master_table, self.master_table_version
|
||||||
|
)?;
|
||||||
|
writeln!(f, " Local Table: v{}", self.local_table_version)?;
|
||||||
|
writeln!(f)?;
|
||||||
|
writeln!(f, " Observation Time:")?;
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
" DateTime: {:04}-{:02}-{:02} {:02}:{:02}:{:02} UTC",
|
||||||
|
self.year, self.month, self.day, self.hour, self.minute, self.second
|
||||||
|
)?;
|
||||||
|
writeln!(f)?;
|
||||||
|
writeln!(f, " Optional Data:")?;
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
" Section 2 Present: {}",
|
||||||
|
if self.optional_section_present {
|
||||||
|
"Yes"
|
||||||
|
} else {
|
||||||
|
"No"
|
||||||
|
}
|
||||||
|
)?;
|
||||||
|
write!(f, " Local Use Data: {} bytes", self.local_use.len())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Section2 {
|
||||||
|
pub length: usize,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_section2(input: &[u8]) -> IResult<&[u8], Section2> {
|
||||||
|
let (input, length) = be_u24(input)?;
|
||||||
|
let (input, _) = skip1(input)?;
|
||||||
|
let (input, data) = take(length - 4)(input)?;
|
||||||
|
Ok((
|
||||||
|
input,
|
||||||
|
Section2 {
|
||||||
|
length: length as usize,
|
||||||
|
data: data.to_vec(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Section3 {
|
||||||
|
pub length: usize,
|
||||||
|
pub number_of_subsets: u16,
|
||||||
|
pub is_observation: bool,
|
||||||
|
pub is_compressed: bool,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_section3(input: &[u8]) -> IResult<&[u8], Section3> {
|
||||||
|
let (input, length) = be_u24(input)?;
|
||||||
|
let (input, _) = skip1(input)?;
|
||||||
|
let (input, number_of_subsets) = be_u16(input)?;
|
||||||
|
let (input, flags) = be_u8(input)?;
|
||||||
|
let is_observation = (flags & 0b1000_0000) != 0;
|
||||||
|
let is_compressed = (flags & 0b0100_0000) != 0;
|
||||||
|
let (input, data) = take(length - 7)(input)?;
|
||||||
|
Ok((
|
||||||
|
input,
|
||||||
|
Section3 {
|
||||||
|
length: length as usize,
|
||||||
|
number_of_subsets,
|
||||||
|
is_observation,
|
||||||
|
is_compressed,
|
||||||
|
data: data.to_vec(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Section4 {
|
||||||
|
pub length: usize,
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_section4(input: &[u8]) -> IResult<&[u8], Section4> {
|
||||||
|
let (input, length) = be_u24(input)?;
|
||||||
|
let (input, _) = skip1(input)?;
|
||||||
|
let (input, data) = take(length - 4)(input)?;
|
||||||
|
Ok((
|
||||||
|
input,
|
||||||
|
Section4 {
|
||||||
|
length: length as usize,
|
||||||
|
data: data.to_vec(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Section5;
|
||||||
|
|
||||||
|
fn parse_section5(input: &[u8]) -> IResult<&[u8], Section5> {
|
||||||
|
let (input, _) = tag("7777")(input)?;
|
||||||
|
Ok((input, Section5 {}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_message(input: &[u8]) -> IResult<&[u8], ()> {
|
||||||
|
let (input, section0) = parse_section0(input)?;
|
||||||
|
let (input, section1) = parse_section1(input)?;
|
||||||
|
let (input, section2) = if section1.optional_section_present {
|
||||||
|
let (input, sec2) = parse_section2(input)?;
|
||||||
|
(input, Some(sec2))
|
||||||
|
} else {
|
||||||
|
(input, None)
|
||||||
|
};
|
||||||
|
let (input, section3) = parse_section3(input)?;
|
||||||
|
let (input, section4) = parse_section4(input)?;
|
||||||
|
let (input, section5) = parse_section5(input)?;
|
||||||
|
|
||||||
|
Ok((input, ()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct BUFRMessage {
|
||||||
|
pub section0: Section0,
|
||||||
|
pub section1: Section1,
|
||||||
|
pub section2: Option<Section2>,
|
||||||
|
pub section3: Section3,
|
||||||
|
pub section4: Section4,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BUFRMessage {
|
||||||
|
pub fn parse(input: &[u8]) -> IResult<&[u8], BUFRMessage> {
|
||||||
|
let (input, section0) = parse_section0(input)?;
|
||||||
|
let (input, section1) = parse_section1(input)?;
|
||||||
|
let (input, section2) = if section1.optional_section_present {
|
||||||
|
let (input, sec2) = parse_section2(input)?;
|
||||||
|
(input, Some(sec2))
|
||||||
|
} else {
|
||||||
|
(input, None)
|
||||||
|
};
|
||||||
|
let (input, section3) = parse_section3(input)?;
|
||||||
|
let (input, section4) = parse_section4(input)?;
|
||||||
|
let (input, _section5) = parse_section5(input)?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
input,
|
||||||
|
BUFRMessage {
|
||||||
|
section0,
|
||||||
|
section1,
|
||||||
|
section2,
|
||||||
|
section3,
|
||||||
|
section4,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
1485
tables/bufr/bufrtabb_11.csv
Normal file
1485
tables/bufr/bufrtabb_11.csv
Normal file
File diff suppressed because it is too large
Load Diff
2404
tables/bufr/bufrtabd_11.csv
Normal file
2404
tables/bufr/bufrtabd_11.csv
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user