// Copyright (C) 2020 Harald Eilertsen // SPDX-FileCopyrightText: 2021 Harald Eilertsen // // SPDX-License-Identifier: GPL-3.0-or-later mod cubase_project; use cubase_project::*; use nom::{ branch::alt, bytes::complete::*, combinator::{map}, multi::{fold_many0, length_data, length_value}, number::complete::*, sequence::{preceded, terminated, tuple}, Finish, IResult, }; use std::{ error::Error, fmt, path::Path, }; /// Parser for length prefixed strings. /// /// Cubase uses a string format where the length of the string is given as a 32 bit big endian word /// before the actual string data. The string may also be zero terminated, with the zero term char /// included in the length. In that case, we trim off the ending zero bytes. /// /// We assume strings to be UTF-8 encoded. fn cmstring(input: &[u8]) -> IResult<&[u8], &str> { let strdata = length_value(be_u32, take_till(|c| c == b'\0')); map(strdata, |s| std::str::from_utf8(s).unwrap())(input) } /** * The data chunks in the file is split into what seems like a structure of containers and leaf * nodes. Where the containers don't contain any data of themselves, but leaf nodes may contain * data and also be a container for further sub nodes. * * Each node regardless of type has a name, and a 16 bit number which meaning I'm not sure about. * Leaf nodes also has a data payload prefixed by a 32 bit length (be). */ #[derive(Clone, Debug, PartialEq)] enum NodeType { Container, Object, } struct Node<'a> { node_type: NodeType, name: &'a str, num: u16, payload: Option<&'a [u8]>, } impl<'a> fmt::Debug for Node<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut r = f.debug_struct("Node"); r.field("node_type", &self.node_type) .field("name", &self.name) .field("num", &self.num); if let Some(p) = self.payload { r.field("payload_size", &p.len()); } r.finish() } } fn p_app_version<'a>(input: &'a [u8]) -> IResult<&'a [u8], PAppVersion> { let mut v = PAppVersion::default(); let (mut r, (appname, appversion, appdate, num2)) = tuple((cmstring, cmstring, cmstring, be_u32))(input)?; v.appname = String::from(appname); v.appversion = String::from(appversion); v.appdate = String::from(appdate); v.num2 = num2; if r.len() > 0 { let (r2, (apparch, num3, appencoding, applocale)) = tuple((cmstring, be_u32, cmstring, cmstring))(&r)?; v.apparch = String::from(apparch); v.num3 = num3; v.appencoding = String::from(appencoding); v.applocale = String::from(applocale); r = r2; } Ok((r, v)) } fn p_arrangement<'a>(input: &'a [u8]) -> IResult<&'a [u8], PArrangement> { fold_many0( node, PArrangement::default(), |a: PArrangement, n: Node<'_>| { println!(" {:?}", n); a } )(input) } /** * Root chunks always have the same layout, two length prefixed strings. * * These seem to be a mapping between a field name, and the data type the field contains. * The actual data follows in the ARCH chunk following this ROOT chunk. */ fn root_chunk<'a>(input: &'a [u8]) -> IResult<&'a [u8], (&str, &str)> { preceded( tag(b"ROOT"), length_value(be_u32, tuple((cmstring, cmstring))))(input) } // fn arch_chunk<'a, O, E, F>(subparser: F) -> impl FnMut(&'a [u8]) -> IResult<&'a [u8], O, E> // where // E: ParseError<&'a [u8]>, // F: Parser<&'a [u8], O, E>, // { // preceded( // tag(b"ARCH"), // length_value(be_u32, subparser)) // } fn arch_chunk<'a>(input: &'a [u8]) -> IResult<&'a [u8], &'a [u8]> { preceded( tag(b"ARCH"), length_data(be_u32) )(input) } /** * A container node does not contain any data of it's own, but contains * one or more sub elements. These can be either other containers, or * object/leaf nodes. */ fn container_node<'a>(data: &'a [u8]) -> IResult<&'a [u8], Node> { let (r, (_, name, num)) = tuple((tag(b"\xff\xff\xff\xfe"), cmstring, be_u16))(data)?; Ok((r, Node { node_type: NodeType::Container, name, num, payload: None} )) } /** * An object node contains serialized structured data. * * It has a size, as well as a payload containing the actual serialized data. */ fn object_node<'a>(data: &'a [u8]) -> IResult<&'a [u8], Node> { let (r, (_, name, num, payload)) = tuple((tag(b"\xff\xff\xff\xff"), cmstring, be_u16, length_data(be_u32)))(data)?; Ok((r, Node { node_type: NodeType::Object, name, num, payload: Some(payload) } )) } fn node<'a>(data: &'a [u8]) -> IResult<&'a [u8], Node> { alt((container_node, object_node))(data) } fn version_chunk<'a>(input: &'a [u8]) -> IResult<&'a [u8], PAppVersion> { fold_many0( node, PAppVersion::default(), |mut version, n: Node<'_>| { println!(" {:?}", n); if n.node_type == NodeType::Object { let (_, v) = p_app_version(n.payload.unwrap()).unwrap(); version = v; } version } )(input) // preceded( // container_node, // p_app_version_node // )(input) } fn arrangement_chunk<'a>(input: &'a [u8]) -> IResult<&'a [u8], PArrangement> { fold_many0( node, PArrangement::default(), |a: PArrangement, n: Node<'_>| { println!(" {:?}", n); if n.node_type == NodeType::Object { let _ = p_arrangement(n.payload.unwrap()); } a } )(input) } /** * Main parser for a Cubase Project (.cpr) file. */ fn cpr_file<'a>(input: &'a [u8]) -> IResult<&'a [u8], CubaseProject> { preceded(cpr_file_header, cpr_file_body)(input) } /** * Parse the Cubase Project File Header. * * A Cubase project file is a RIFF file. * * That is almost. There's a strange extra tag "NUND" between the root (RIFF) header and the * rest of the chunks. This would have been ok, if the NUND tag was followed by a length field, * as it would just be another chunk. This is not the case however, the tag is followed * immefiately by another chunk header. * * To make it even worse, the chunk size of the root chunk is four bytes short, so this makes * the file parser itself a bit more complex than what it needs to be. * * We just swallow the extra "NUND" tag here, so it won't bother us later. */ fn cpr_file_header<'a>(input: &'a [u8]) -> IResult<&'a [u8], u32> { terminated( preceded(tag(b"RIFF"), be_u32), tag(b"NUND") )(input) } fn cpr_file_body<'a>(input: &'a [u8]) -> IResult<&'a [u8], CubaseProject> { fold_many0( tuple((root_chunk, arch_chunk)), CubaseProject::default(), |mut prj, ((key, value), arch)| { println!("Root: {} = {}", key, value); match key { "Version" => { let (_, v) = version_chunk(arch).unwrap(); prj.app_version = v; }, "Arrangement1" => { let (_, a) = arrangement_chunk(arch).unwrap(); prj.arrangement = a; } _ => {}, } prj } )(input) } pub fn parse_cubase_project

(filename: P) -> Result> where P: AsRef { println!("Reading {}...", filename.as_ref().to_str().ok_or("Invalid file name")?); let data = std::fs::read(filename)?; let (_, proj) = cpr_file(&data) .finish() .map_err(|e| format!("{:?}", e))?; println!("[*] Done!"); Ok(proj) } fn main() -> Result<(), Box> { let filename = std::env::args() .skip(1) .next() .expect("You must give me a file to analyze"); let proj = parse_cubase_project(filename)?; println!("File generated by {} {}", proj.app_version.appname, proj.app_version.appversion); println!("Architecture: {}", proj.app_version.apparch); println!("Encoding: {}", proj.app_version.appencoding); println!("Locale: {}", proj.app_version.applocale); Ok(()) }