// Copyright (C) 2020 Harald Eilertsen
// SPDX-FileCopyrightText: 2021 Harald Eilertsen <haraldei@anduin.net>
//
// 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<P>(filename: P) -> Result<CubaseProject, Box<dyn Error>>
where
P: AsRef<Path>
{
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<dyn Error>> {
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(())
}