aboutsummaryrefslogblamecommitdiffstats
path: root/src/main.rs
blob: 3b852d41a9e35377bd31d34459bacec5e38a3e18 (plain) (tree)
1
2
3
4
5
6
7
8
9



                                                                      
 



                      
          
                
                       

                                                   
                        
                                            



            
                 
        




                                       


                                                                                                   

                                          
                                                   
                                                                  
                                                            

 







                                                                                                 
                                  


               

 
 

                        
                  

                              

 















                                                              
                                                                         
                                       
 
                                                                                                              
 



                                            
 

                                                                                                               
 



                                                  
 
               
     

              

 
                                                                          
 








                                        

 
   
                                                                        



                                                                                         



                                                                       

 











                                                                                              

                     

                           
 
 
 




                                                                       
                                                                  
                                                                                         
                                                                              






                                                                             

                                                                                                                       
                                                                                     

 


                                                        
 









                                                                         
             
                   
         





                             
 



















                                                                              

 
   
                                        






                                                                                               
  

                                                                                             

                                                                          
   

                                                                   


                                       

 



















                                                                           

 






                                                                                      
                                   


                                          
                          

            

 
                                         




                                                      





                                                                                               


          
// 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(())
}