diff options
Diffstat (limited to 'src/VBox/Storage/CUE.cpp')
-rw-r--r-- | src/VBox/Storage/CUE.cpp | 1971 |
1 files changed, 1971 insertions, 0 deletions
diff --git a/src/VBox/Storage/CUE.cpp b/src/VBox/Storage/CUE.cpp new file mode 100644 index 00000000..72e6f6f1 --- /dev/null +++ b/src/VBox/Storage/CUE.cpp @@ -0,0 +1,1971 @@ +/* $Id: CUE.cpp $ */ +/** @file + * CUE - CUE/BIN Disk image, Core Code. + */ + +/* + * Copyright (C) 2017-2022 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, in version 3 of the + * License. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <https://www.gnu.org/licenses>. + * + * SPDX-License-Identifier: GPL-3.0-only + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_VD_CUE +#include <VBox/vd-plugin.h> +#include <VBox/err.h> + +#include <VBox/log.h> +#include <VBox/scsiinline.h> +#include <iprt/assert.h> +#include <iprt/asm.h> +#include <iprt/alloc.h> +#include <iprt/cdefs.h> +#include <iprt/ctype.h> +#include <iprt/path.h> +#include <iprt/string.h> + +#include "VDBackends.h" +#include "VDBackendsInline.h" + + +/********************************************************************************************************************************* +* Constants And Macros, Structures and Typedefs * +*********************************************************************************************************************************/ + +/** + * CUE descriptor file token type. + */ +typedef enum CUETOKENTYPE +{ + /** Invalid token type. */ + CUETOKENTYPE_INVALID = 0, + /** Reserved keyword. */ + CUETOKENTYPE_KEYWORD, + /** String token. */ + CUETOKENTYPE_STRING, + /** Unsigned integer. */ + CUETOKENTYPE_INTEGER_UNSIGNED, + /** MSF (mm:ss:ff) location token. */ + CUETOKENTYPE_MSF, + /** Error token (unexpected character found). */ + CUETOKENTYPE_ERROR, + /** End of stream token. */ + CUETOKENTYPE_EOS +} CUETOKENTYPE; + +/** + * CUE reservered keyword type. + */ +typedef enum CUEKEYWORD +{ + /** Invalid keyword. */ + CUEKEYWORD_INVALID = 0, + /** FILE. */ + CUEKEYWORD_FILE, + /** BINARY */ + CUEKEYWORD_BINARY, + /** MOTOROLA */ + CUEKEYWORD_MOTOROLA, + /** WAVE */ + CUEKEYWORD_WAVE, + /** MP3 */ + CUEKEYWORD_MP3, + /** AIFF */ + CUEKEYWORD_AIFF, + /** CATALOG */ + CUEKEYWORD_CATALOG, + CUEKEYWORD_CDTEXTFILE, + CUEKEYWORD_FLAGS, + CUEKEYWORD_INDEX, + CUEKEYWORD_ISRC, + CUEKEYWORD_PERFORMER, + CUEKEYWORD_POSTGAP, + CUEKEYWORD_PREGAP, + CUEKEYWORD_SONGWRITER, + CUEKEYWORD_TITLE, + CUEKEYWORD_TRACK, + CUEKEYWORD_MODE1_2048, + CUEKEYWORD_MODE1_2352, + CUEKEYWORD_MODE2_2352, + CUEKEYWORD_AUDIO, + CUEKEYWORD_REM +} CUEKEYWORD; + +/** + * CUE sheet token. + */ +typedef struct CUETOKEN +{ + /** The token type. */ + CUETOKENTYPE enmType; + /** Token type dependent data. */ + union + { + /** Keyword token. */ + struct + { + /** The keyword enumerator. */ + CUEKEYWORD enmKeyword; + } Keyword; + /** String token (without quotation marks). */ + struct + { + /** Pointer to the start of the string. */ + const char *psz; + /** Number of characters for the string excluding the null terminator. */ + size_t cch; + } String; + /** Integer token. */ + struct + { + /** Numerical constant. */ + uint64_t u64; + } Int; + /** MSF location token. */ + struct + { + /** Minute part. */ + uint8_t u8Minute; + /** Second part. */ + uint8_t u8Second; + /** Frame part. */ + uint8_t u8Frame; + } Msf; + } Type; +} CUETOKEN; +/** Pointer to a CUE sheet token. */ +typedef CUETOKEN *PCUETOKEN; +/** Pointer to a const CUE sheet token. */ +typedef const CUETOKEN *PCCUETOKEN; + +/** + * CUE tokenizer state. + */ +typedef struct CUETOKENIZER +{ + /** Char buffer to read from. */ + const char *pszInput; + /** Token 1. */ + CUETOKEN Token1; + /** Token 2. */ + CUETOKEN Token2; + /** Pointer to the current active token. */ + PCUETOKEN pTokenCurr; + /** The next token in the input stream (used for peeking). */ + PCUETOKEN pTokenNext; +} CUETOKENIZER; +/** Pointer to a CUE tokenizer state. */ +typedef CUETOKENIZER *PCUETOKENIZER; + +/** + * CUE keyword entry. + */ +typedef struct CUEKEYWORDDESC +{ + /** Keyword string. */ + const char *pszKeyword; + /** Size of the string in characters without zero terminator. */ + size_t cchKeyword; + /** Keyword type. */ + CUEKEYWORD enmKeyword; +} CUEKEYWORDDESC; +/** Pointer to a CUE keyword entry. */ +typedef CUEKEYWORDDESC *PCUEKEYWORDDESC; +/** Pointer to a const CUE keyword entry. */ +typedef const CUEKEYWORDDESC *PCCUEKEYWORDDESC; + +/** + * CUE image data structure. + */ +typedef struct CUEIMAGE +{ + /** Image name. */ + const char *pszFilename; + /** Storage handle. */ + PVDIOSTORAGE pStorage; + /** The backing file containing the actual data. */ + char *pszDataFilename; + /** Storage handle for the backing file. */ + PVDIOSTORAGE pStorageData; + + /** Pointer to the per-disk VD interface list. */ + PVDINTERFACE pVDIfsDisk; + /** Pointer to the per-image VD interface list. */ + PVDINTERFACE pVDIfsImage; + /** Error interface. */ + PVDINTERFACEERROR pIfError; + /** I/O interface. */ + PVDINTERFACEIOINT pIfIo; + + /** Open flags passed by VD layer. */ + unsigned uOpenFlags; + /** Image flags defined during creation or determined during open. */ + unsigned uImageFlags; + /** Maximum number of tracks the region list can hold. */ + uint32_t cTracksMax; + /** Pointer to our internal region list. */ + PVDREGIONLIST pRegionList; + /** Flag whether the backing file is little (BINARY) or big (MOTOROLA) endian. */ + bool fLittleEndian; +} CUEIMAGE, *PCUEIMAGE; + + +/********************************************************************************************************************************* +* Static Variables * +*********************************************************************************************************************************/ + +/** NULL-terminated array of supported file extensions. */ +static const VDFILEEXTENSION s_aCueFileExtensions[] = +{ + {"cue", VDTYPE_OPTICAL_DISC}, + {NULL, VDTYPE_INVALID} +}; + +/** + * Known keywords. + */ +static const CUEKEYWORDDESC g_aCueKeywords[] = +{ + {RT_STR_TUPLE("FILE"), CUEKEYWORD_FILE}, + {RT_STR_TUPLE("BINARY"), CUEKEYWORD_BINARY}, + {RT_STR_TUPLE("MOTOROLA"), CUEKEYWORD_MOTOROLA}, + {RT_STR_TUPLE("WAVE"), CUEKEYWORD_WAVE}, + {RT_STR_TUPLE("MP3"), CUEKEYWORD_MP3}, + {RT_STR_TUPLE("AIFF"), CUEKEYWORD_AIFF}, + {RT_STR_TUPLE("CATALOG"), CUEKEYWORD_CATALOG}, + {RT_STR_TUPLE("CDTEXTFILE"), CUEKEYWORD_CDTEXTFILE}, + {RT_STR_TUPLE("FLAGS"), CUEKEYWORD_FLAGS}, + {RT_STR_TUPLE("INDEX"), CUEKEYWORD_INDEX}, + {RT_STR_TUPLE("ISRC"), CUEKEYWORD_ISRC}, + {RT_STR_TUPLE("PERFORMER"), CUEKEYWORD_PERFORMER}, + {RT_STR_TUPLE("POSTGAP"), CUEKEYWORD_POSTGAP}, + {RT_STR_TUPLE("PREGAP"), CUEKEYWORD_PREGAP}, + {RT_STR_TUPLE("SONGWRITER"), CUEKEYWORD_SONGWRITER}, + {RT_STR_TUPLE("TITLE"), CUEKEYWORD_TITLE}, + {RT_STR_TUPLE("TRACK"), CUEKEYWORD_TRACK}, + {RT_STR_TUPLE("MODE1/2048"), CUEKEYWORD_MODE1_2048}, + {RT_STR_TUPLE("MODE1/2352"), CUEKEYWORD_MODE1_2352}, + {RT_STR_TUPLE("MODE2/2352"), CUEKEYWORD_MODE2_2352}, + {RT_STR_TUPLE("AUDIO"), CUEKEYWORD_AUDIO}, + {RT_STR_TUPLE("REM"), CUEKEYWORD_REM} +}; + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ + + +/** + * Converts a MSF formatted address value read from the given buffer + * to an LBA number. MSF 00:00:00 equals LBA 0. + * + * @returns The LBA number. + * @param pbBuf The buffer to read the MSF formatted address + * from. + */ +DECLINLINE(uint32_t) cueMSF2LBA(const uint8_t *pbBuf) +{ + return (pbBuf[0] * 60 + pbBuf[1]) * 75 + pbBuf[2]; +} + +/** + * Ensures that the region list can hold up to the given number of tracks. + * + * @returns VBox status code. + * @param pThis The CUE image state. + * @param cTracksMax Maximum number of tracks. + */ +static int cueEnsureRegionListSize(PCUEIMAGE pThis, uint32_t cTracksMax) +{ + int rc = VINF_SUCCESS; + + if (pThis->cTracksMax < cTracksMax) + { + PVDREGIONLIST pRegionListNew = (PVDREGIONLIST)RTMemRealloc(pThis->pRegionList, + RT_UOFFSETOF_DYN(VDREGIONLIST, aRegions[cTracksMax])); + if (pRegionListNew) + { + /* Init all the new allocated tracks. */ + for (uint32_t i = pThis->cTracksMax; i < cTracksMax; i++) + pRegionListNew->aRegions[i].offRegion = UINT64_MAX; + pThis->pRegionList = pRegionListNew; + pThis->cTracksMax = cTracksMax; + } + else + rc = VERR_NO_MEMORY; + } + + return rc; +} + +/** + * Returns whether the tokenizer reached the end of the stream. + * + * @returns true if the tokenizer reached the end of stream marker + * false otherwise. + * @param pTokenizer The tokenizer state. + */ +DECLINLINE(bool) cueTokenizerIsEos(PCUETOKENIZER pTokenizer) +{ + return *pTokenizer->pszInput == '\0'; +} + +/** + * Skip one character in the input stream. + * + * @returns nothing. + * @param pTokenizer The tokenizer state. + */ +DECLINLINE(void) cueTokenizerSkipCh(PCUETOKENIZER pTokenizer) +{ + /* Never ever go past EOS. */ + if (!cueTokenizerIsEos(pTokenizer)) + pTokenizer->pszInput++; +} + +/** + * Returns the next char in the input buffer without advancing it. + * + * @returns Next character in the input buffer. + * @param pTokenizer The tokenizer state. + */ +DECLINLINE(char) cueTokenizerPeekCh(PCUETOKENIZER pTokenizer) +{ + return cueTokenizerIsEos(pTokenizer) + ? '\0' + : *(pTokenizer->pszInput + 1); +} + +/** + * Returns the next character in the input buffer advancing the internal + * position. + * + * @returns Next character in the stream. + * @param pTokenizer The tokenizer state. + */ +DECLINLINE(char) cueTokenizerGetCh(PCUETOKENIZER pTokenizer) +{ + char ch; + + if (cueTokenizerIsEos(pTokenizer)) + ch = '\0'; + else + ch = *pTokenizer->pszInput; + + return ch; +} + +/** + * Sets a new line for the tokenizer. + * + * @returns nothing. + * @param pTokenizer The tokenizer state. + * @param cSkip How many characters to skip. + */ +DECLINLINE(void) cueTokenizerNewLine(PCUETOKENIZER pTokenizer, unsigned cSkip) +{ + pTokenizer->pszInput += cSkip; +} + +/** + * Checks whether the current position in the input stream is a new line + * and skips it. + * + * @returns Flag whether there was a new line at the current position + * in the input buffer. + * @param pTokenizer The tokenizer state. + */ +DECLINLINE(bool) cueTokenizerIsSkipNewLine(PCUETOKENIZER pTokenizer) +{ + bool fNewline = true; + + if ( cueTokenizerGetCh(pTokenizer) == '\r' + && cueTokenizerPeekCh(pTokenizer) == '\n') + cueTokenizerNewLine(pTokenizer, 2); + else if (cueTokenizerGetCh(pTokenizer) == '\n') + cueTokenizerNewLine(pTokenizer, 1); + else + fNewline = false; + + return fNewline; +} + +/** + * Skip all whitespace starting from the current input buffer position. + * Skips all present comments too. + * + * @returns nothing. + * @param pTokenizer The tokenizer state. + */ +DECLINLINE(void) cueTokenizerSkipWhitespace(PCUETOKENIZER pTokenizer) +{ + while (!cueTokenizerIsEos(pTokenizer)) + { + while ( cueTokenizerGetCh(pTokenizer) == ' ' + || cueTokenizerGetCh(pTokenizer) == '\t') + cueTokenizerSkipCh(pTokenizer); + + if ( !cueTokenizerIsEos(pTokenizer) + && !cueTokenizerIsSkipNewLine(pTokenizer)) + break; /* Skipped everything, next is some real content. */ + } +} + +/** + * Skips a multi line comment. + * + * @returns nothing. + * @param pTokenizer The tokenizer state. + */ +DECLINLINE(void) cueTokenizerSkipComment(PCUETOKENIZER pTokenizer) +{ + while ( !cueTokenizerIsEos(pTokenizer) + && !cueTokenizerIsSkipNewLine(pTokenizer)) + cueTokenizerSkipCh(pTokenizer); + cueTokenizerSkipWhitespace(pTokenizer); +} + +/** + * Get an identifier token from the tokenizer. + * + * @returns nothing. + * @param pTokenizer The tokenizer state. + * @param pToken The uninitialized token. + */ +static void cueTokenizerGetKeyword(PCUETOKENIZER pTokenizer, PCUETOKEN pToken) +{ + char ch; + unsigned cchKeyword = 0; + bool fIsKeyword = false; + bool fIsComment; + const char *pszKeyword; + + Assert(RT_C_IS_ALPHA(*pTokenizer->pszInput)); + + do + { + fIsComment = false; + pszKeyword = pTokenizer->pszInput; + + do + { + cchKeyword++; + cueTokenizerSkipCh(pTokenizer); + ch = cueTokenizerGetCh(pTokenizer); + } + while (RT_C_IS_ALNUM(ch) || ch == '_' || ch == '/' || ch == '.'); + + /* Check whether we got a keyword or a string constant. */ + for (unsigned i = 0; i < RT_ELEMENTS(g_aCueKeywords); i++) + { + if (!RTStrNCmp(g_aCueKeywords[i].pszKeyword, pszKeyword, RT_MIN(cchKeyword, g_aCueKeywords[i].cchKeyword))) + { + if (g_aCueKeywords[i].enmKeyword == CUEKEYWORD_REM) + { + /* The REM keyword is handled here as it indicates a comment which we just skip. */ + cueTokenizerSkipComment(pTokenizer); + fIsComment = true; + } + else + { + fIsKeyword = true; + pToken->enmType = CUETOKENTYPE_KEYWORD; + pToken->Type.Keyword.enmKeyword = g_aCueKeywords[i].enmKeyword; + } + break; + } + } + } while (fIsComment); + + /* Make it a string. */ + if (ch == '\0') + pToken->enmType = CUETOKENTYPE_EOS; + else if (!fIsKeyword) + { + pToken->enmType = CUETOKENTYPE_STRING; + pToken->Type.String.psz = pszKeyword; + pToken->Type.String.cch = cchKeyword; + } +} + +/** + * Get an integer value or MSF location indicator from the tokenizer. + * + * @returns nothing. + * @param pTokenizer The tokenizer state. + * @param pToken The uninitialized token. + */ +static void cueTokenizerGetIntegerOrMsf(PCUETOKENIZER pTokenizer, PCUETOKEN pToken) +{ + char szInt[20 + 1]; /* Maximum which fits into an unsigned 64bit integer + zero terminator. */ + unsigned cchInt = 0; + bool fMsf = false; + char ch = cueTokenizerGetCh(pTokenizer); + + Assert(RT_C_IS_DIGIT(ch)); + RT_ZERO(szInt); + + /* Go through the characters and check for the : mark which denotes MSF location indicator. */ + do + { + szInt[cchInt++] = ch; + cueTokenizerSkipCh(pTokenizer); + ch = cueTokenizerGetCh(pTokenizer); + if (ch == ':') + fMsf = true; + } + while ( (RT_C_IS_DIGIT(ch) || ch == ':') + && cchInt < sizeof(szInt)); + + if (cchInt < sizeof(szInt) - 1) + { + if (fMsf) + { + /* Check that the format matches our expectations (mm:ss:ff). */ + if ( cchInt == 8 && szInt[2] == ':' && szInt[5] == ':') + { + /* Parse the single fields. */ + szInt[2] = '\0'; + szInt[5] = '\0'; + + int rc = RTStrToUInt8Full(&szInt[0], 10, &pToken->Type.Msf.u8Minute); + if (RT_SUCCESS(rc)) + rc = RTStrToUInt8Full(&szInt[3], 10, &pToken->Type.Msf.u8Second); + if (RT_SUCCESS(rc)) + rc = RTStrToUInt8Full(&szInt[6], 10, &pToken->Type.Msf.u8Frame); + if (RT_SUCCESS(rc)) + pToken->enmType = CUETOKENTYPE_MSF; + else + pToken->enmType = CUETOKENTYPE_ERROR; + } + else + pToken->enmType = CUETOKENTYPE_ERROR; + } + else + { + pToken->enmType = CUETOKENTYPE_INTEGER_UNSIGNED; + int rc = RTStrToUInt64Full(&szInt[0], 10, &pToken->Type.Int.u64); + if (RT_FAILURE(rc)) + pToken->enmType = CUETOKENTYPE_ERROR; + } + } + else + pToken->enmType = CUETOKENTYPE_ERROR; +} + +/** + * Parses a string constant. + * + * @returns nothing. + * @param pTokenizer The tokenizer state. + * @param pToken The uninitialized token. + * + * @remarks: No escape sequences allowed at this time. + */ +static void cueTokenizerGetStringConst(PCUETOKENIZER pTokenizer, PCUETOKEN pToken) +{ + unsigned cchStr = 0; + + Assert(cueTokenizerGetCh(pTokenizer) == '\"'); + cueTokenizerSkipCh(pTokenizer); /* Skip " */ + + pToken->enmType = CUETOKENTYPE_STRING; + pToken->Type.String.psz = pTokenizer->pszInput; + + while ( !cueTokenizerIsEos(pTokenizer) + && cueTokenizerGetCh(pTokenizer) != '\"') + { + cchStr++; + cueTokenizerSkipCh(pTokenizer); + } + + /* End of stream without a closing quote is an error. */ + if (RT_UNLIKELY(cueTokenizerIsEos(pTokenizer))) + pToken->enmType = CUETOKENTYPE_ERROR; + else + { + cueTokenizerSkipCh(pTokenizer); /* Skip closing " */ + pToken->Type.String.cch = cchStr; + } +} + +/** + * Get the end of stream token. + * + * @returns nothing. + * @param pTokenizer The tokenizer state. + * @param pToken The uninitialized token. + */ +static void cueTokenizerGetEos(PCUETOKENIZER pTokenizer, PCUETOKEN pToken) +{ + Assert(cueTokenizerGetCh(pTokenizer) == '\0'); RT_NOREF(pTokenizer); + + pToken->enmType = CUETOKENTYPE_EOS; +} + +/** + * Read the next token from the tokenizer stream. + * + * @returns nothing. + * @param pTokenizer The tokenizer to read from. + * @param pToken Uninitialized token to fill the token data into. + */ +static void cueTokenizerReadNextToken(PCUETOKENIZER pTokenizer, PCUETOKEN pToken) +{ + /* Skip all eventually existing whitespace, newlines and comments first. */ + cueTokenizerSkipWhitespace(pTokenizer); + + char ch = cueTokenizerGetCh(pTokenizer); + if (RT_C_IS_ALPHA(ch)) + cueTokenizerGetKeyword(pTokenizer, pToken); + else if (RT_C_IS_DIGIT(ch)) + cueTokenizerGetIntegerOrMsf(pTokenizer, pToken); + else if (ch == '\"') + cueTokenizerGetStringConst(pTokenizer, pToken); + else if (ch == '\0') + cueTokenizerGetEos(pTokenizer, pToken); + else + pToken->enmType = CUETOKENTYPE_ERROR; +} + +/** + * Create a new tokenizer. + * + * @returns Pointer to the new tokenizer state on success. + * NULL if out of memory. + * @param pszInput The input to create the tokenizer for. + */ +static PCUETOKENIZER cueTokenizerCreate(const char *pszInput) +{ + PCUETOKENIZER pTokenizer = (PCUETOKENIZER)RTMemAllocZ(sizeof(CUETOKENIZER)); + if (pTokenizer) + { + pTokenizer->pszInput = pszInput; + pTokenizer->pTokenCurr = &pTokenizer->Token1; + pTokenizer->pTokenNext = &pTokenizer->Token2; + /* Fill the tokenizer with two first tokens. */ + cueTokenizerReadNextToken(pTokenizer, pTokenizer->pTokenCurr); + cueTokenizerReadNextToken(pTokenizer, pTokenizer->pTokenNext); + } + + return pTokenizer; +} + +/** + * Get the current token in the input stream. + * + * @returns Pointer to the next token in the stream. + * @param pTokenizer The tokenizer to destroy. + */ +DECLINLINE(PCCUETOKEN) cueTokenizerGetToken(PCUETOKENIZER pTokenizer) +{ + return pTokenizer->pTokenCurr; +} + +/** + * Get the class of the current token. + * + * @returns Class of the current token. + * @param pTokenizer The tokenizer state. + */ +DECLINLINE(CUETOKENTYPE) cueTokenizerGetTokenType(PCUETOKENIZER pTokenizer) +{ + return pTokenizer->pTokenCurr->enmType; +} + +/** + * Consume the current token advancing to the next in the stream. + * + * @returns nothing. + * @param pTokenizer The tokenizer state. + */ +static void cueTokenizerConsume(PCUETOKENIZER pTokenizer) +{ + PCUETOKEN pTokenTmp = pTokenizer->pTokenCurr; + + /* Switch next token to current token and read in the next token. */ + pTokenizer->pTokenCurr = pTokenizer->pTokenNext; + pTokenizer->pTokenNext = pTokenTmp; + cueTokenizerReadNextToken(pTokenizer, pTokenizer->pTokenNext); +} + +/** + * Check whether the next token in the input stream is a keyword and matches the given + * keyword. + * + * @returns true if the token matched. + * false otherwise. + * @param pTokenizer The tokenizer state. + * @param enmKeyword The keyword to check against. + */ +static bool cueTokenizerIsKeywordEqual(PCUETOKENIZER pTokenizer, CUEKEYWORD enmKeyword) +{ + PCCUETOKEN pToken = cueTokenizerGetToken(pTokenizer); + + if ( pToken->enmType == CUETOKENTYPE_KEYWORD + && pToken->Type.Keyword.enmKeyword == enmKeyword) + return true; + + return false; +} + +/** + * Check whether the next token in the input stream is a keyword and matches the given + * keyword and skips it. + * + * @returns true if the token matched and was skipped. + * false otherwise. + * @param pTokenizer The tokenizer state. + * @param enmKeyword The keyword to check against. + */ +static bool cueTokenizerSkipIfIsKeywordEqual(PCUETOKENIZER pTokenizer, CUEKEYWORD enmKeyword) +{ + bool fEqual = cueTokenizerIsKeywordEqual(pTokenizer, enmKeyword); + if (fEqual) + cueTokenizerConsume(pTokenizer); + + return fEqual; +} + +/** + * Duplicates the string of the current token and consumes it. + * + * @returns VBox status code. + * @param pTokenizer The tokenizer state. + * @param ppszStr Where to store the pointer to the duplicated string on success. + * Free with RTStrFree(). + */ +static int cueTokenizerConsumeStringDup(PCUETOKENIZER pTokenizer, char **ppszStr) +{ + int rc = VINF_SUCCESS; + Assert(cueTokenizerGetTokenType(pTokenizer) == CUETOKENTYPE_STRING); + + *ppszStr = RTStrDupN(pTokenizer->pTokenCurr->Type.String.psz, + pTokenizer->pTokenCurr->Type.String.cch); + if (!*ppszStr) + rc = VERR_NO_STR_MEMORY; + + cueTokenizerConsume(pTokenizer); + return rc; +} + +/** + * Consumes an integer token returning the value. + * + * @returns Integer value in the token. + * @param pTokenizer The tokenizer state. + */ +static uint64_t cueTokenizerConsumeInteger(PCUETOKENIZER pTokenizer) +{ + uint64_t u64 = 0; + Assert(cueTokenizerGetTokenType(pTokenizer) == CUETOKENTYPE_INTEGER_UNSIGNED); + + u64 = pTokenizer->pTokenCurr->Type.Int.u64; + cueTokenizerConsume(pTokenizer); + return u64; +} + +/** + * Parses and skips the remaining string part of a directive. + * + * @returns VBox status code. + * @param pThis The CUE image state. + * @param pTokenizer The tokenizer state. + * @param pszDirective The directive we skip the string part for. + */ +static int cueParseAndSkipStringRemainder(PCUEIMAGE pThis, PCUETOKENIZER pTokenizer, + const char *pszDirective) +{ + int rc = VINF_SUCCESS; + + if (cueTokenizerGetTokenType(pTokenizer) == CUETOKENTYPE_STRING) + cueTokenizerConsume(pTokenizer); + else + rc = vdIfError(pThis->pIfError, VERR_NOT_SUPPORTED, RT_SRC_POS, + N_("CUE: Error parsing '%s', expected string for %s directive"), pThis->pszFilename, + pszDirective); + + return rc; +} + +/** + * Parses and skips the remaining MSF part of a directive. + * + * @returns VBox status code. + * @param pThis The CUE image state. + * @param pTokenizer The tokenizer state. + * @param pszDirective The directive we skip the string part for. + */ +static int cueParseAndSkipMsfRemainder(PCUEIMAGE pThis, PCUETOKENIZER pTokenizer, + const char *pszDirective) +{ + int rc = VINF_SUCCESS; + + if (cueTokenizerGetTokenType(pTokenizer) == CUETOKENTYPE_MSF) + cueTokenizerConsume(pTokenizer); + else + rc = vdIfError(pThis->pIfError, VERR_NOT_SUPPORTED, RT_SRC_POS, + N_("CUE: Error parsing '%s', expected MSF location for %s directive"), pThis->pszFilename, + pszDirective); + + return rc; +} + +/** + * Parses the remainder of a INDEX directive. + * + * @returns VBox status code. + * @param pThis The CUE image state. + * @param pTokenizer The tokenizer state. + * @param pu8Index Where to store the parsed index number on success. + * @param pu64Lba Where to store the parsed positional information on success. + */ +static int cueParseIndex(PCUEIMAGE pThis, PCUETOKENIZER pTokenizer, + uint8_t *pu8Index, uint64_t *pu64Lba) +{ + int rc = VINF_SUCCESS; + + /* + * The index consists of the index number and positional information in MSF format. + */ + if (cueTokenizerGetTokenType(pTokenizer) == CUETOKENTYPE_INTEGER_UNSIGNED) + { + uint64_t u64Index = cueTokenizerConsumeInteger(pTokenizer); + if (u64Index <= 99) + { + /* Parse the position. */ + if (cueTokenizerGetTokenType(pTokenizer) == CUETOKENTYPE_MSF) + { + PCCUETOKEN pToken = cueTokenizerGetToken(pTokenizer); + uint8_t abMsf[3]; + abMsf[0] = pToken->Type.Msf.u8Minute; + abMsf[1] = pToken->Type.Msf.u8Second; + abMsf[2] = pToken->Type.Msf.u8Frame; + + *pu8Index = (uint8_t)u64Index; + *pu64Lba = cueMSF2LBA(&abMsf[0]); + cueTokenizerConsume(pTokenizer); + } + else + rc = vdIfError(pThis->pIfError, VERR_NOT_SUPPORTED, RT_SRC_POS, + N_("CUE: Error parsing '%s', expected MSF location"), pThis->pszFilename); + } + else + rc = vdIfError(pThis->pIfError, VERR_NOT_SUPPORTED, RT_SRC_POS, + N_("CUE: Error parsing '%s', index number must be between 01 and 99"), pThis->pszFilename); + } + else + rc = vdIfError(pThis->pIfError, VERR_NOT_SUPPORTED, RT_SRC_POS, + N_("CUE: Error parsing '%s', expected index number after INDEX directive"), pThis->pszFilename); + + return rc; +} + +/** + * Parses the things coming below a TRACK directive. + * + * @returns VBox status code. + * @param pThis The CUE image state. + * @param pTokenizer The tokenizer state. + * @param pu64LbaStart Where to store the starting LBA for this track on success. + */ +static int cueParseTrackNesting(PCUEIMAGE pThis, PCUETOKENIZER pTokenizer, uint64_t *pu64LbaStart) +{ + int rc = VINF_SUCCESS; + bool fSeenInitialIndex = false; + + do + { + if ( cueTokenizerIsKeywordEqual(pTokenizer, CUEKEYWORD_TRACK) + || cueTokenizerGetTokenType(pTokenizer) == CUETOKENTYPE_EOS) + break; + + if (cueTokenizerGetTokenType(pTokenizer) == CUETOKENTYPE_KEYWORD) + { + if (cueTokenizerSkipIfIsKeywordEqual(pTokenizer, CUEKEYWORD_TITLE)) + rc = cueParseAndSkipStringRemainder(pThis, pTokenizer, "TITLE"); + else if (cueTokenizerSkipIfIsKeywordEqual(pTokenizer, CUEKEYWORD_PERFORMER)) + rc = cueParseAndSkipStringRemainder(pThis, pTokenizer, "PERFORMER"); + else if (cueTokenizerSkipIfIsKeywordEqual(pTokenizer, CUEKEYWORD_PREGAP)) + rc = cueParseAndSkipMsfRemainder(pThis, pTokenizer, "PREGAP"); + else if (cueTokenizerSkipIfIsKeywordEqual(pTokenizer, CUEKEYWORD_POSTGAP)) + rc = cueParseAndSkipMsfRemainder(pThis, pTokenizer, "POSTGAP"); + else if (cueTokenizerSkipIfIsKeywordEqual(pTokenizer, CUEKEYWORD_INDEX)) + { + uint8_t u8Index = 0; + uint64_t u64Lba = 0; + rc = cueParseIndex(pThis, pTokenizer, &u8Index, &u64Lba); + if ( RT_SUCCESS(rc) + && u8Index == 1) + { + if (!fSeenInitialIndex) + { + fSeenInitialIndex = true; + *pu64LbaStart = u64Lba; + } + else + rc = vdIfError(pThis->pIfError, VERR_NOT_SUPPORTED, RT_SRC_POS, + N_("CUE: Error parsing '%s', multiple INDEX 01 directives"), pThis->pszFilename); + } + } + else + rc = vdIfError(pThis->pIfError, VERR_NOT_SUPPORTED, RT_SRC_POS, + N_("CUE: Error parsing '%s', unexpected directive for TRACK found"), pThis->pszFilename); + } + else + rc = vdIfError(pThis->pIfError, VERR_NOT_SUPPORTED, RT_SRC_POS, + N_("CUE: Error parsing '%s', expected a CUE sheet keyword"), pThis->pszFilename); + } + while (RT_SUCCESS(rc)); + + if ( RT_SUCCESS(rc) + && !fSeenInitialIndex) + rc = vdIfError(pThis->pIfError, VERR_NOT_SUPPORTED, RT_SRC_POS, + N_("CUE: Error parsing '%s', no initial INDEX directive for this track"), pThis->pszFilename); + + return rc; +} + +/** + * Parses the remainder of a TRACK directive. + * + * @returns VBox status code. + * @param pThis The CUE image state. + * @param pTokenizer The tokenizer state. + */ +static int cueParseTrack(PCUEIMAGE pThis, PCUETOKENIZER pTokenizer) +{ + int rc = VINF_SUCCESS; + + /* + * A track consists of the track number and data type followed by a list of indexes + * and other metadata like title and performer we don't care about. + */ + if (cueTokenizerGetTokenType(pTokenizer) == CUETOKENTYPE_INTEGER_UNSIGNED) + { + uint64_t u64Track = cueTokenizerConsumeInteger(pTokenizer); + if (u64Track <= 99) + { + /* Parse the data mode. */ + if (cueTokenizerGetTokenType(pTokenizer) == CUETOKENTYPE_KEYWORD) + { + CUEKEYWORD enmDataMode = pTokenizer->pTokenCurr->Type.Keyword.enmKeyword; + if ( cueTokenizerSkipIfIsKeywordEqual(pTokenizer, CUEKEYWORD_AUDIO) + || cueTokenizerSkipIfIsKeywordEqual(pTokenizer, CUEKEYWORD_MODE1_2048) + || cueTokenizerSkipIfIsKeywordEqual(pTokenizer, CUEKEYWORD_MODE1_2352) + || cueTokenizerSkipIfIsKeywordEqual(pTokenizer, CUEKEYWORD_MODE2_2352)) + { + /* + * Parse everything coming below the track (index points, etc.), we only need to find + * the starting point. + */ + uint64_t uLbaStart = 0; + rc = cueParseTrackNesting(pThis, pTokenizer, &uLbaStart); + if (RT_SUCCESS(rc)) + { + /* Create a new region for this track. */ + RT_NOREF1(enmDataMode); + rc = cueEnsureRegionListSize(pThis, u64Track); + if (RT_SUCCESS(rc)) + { + PVDREGIONDESC pRegion = &pThis->pRegionList->aRegions[u64Track - 1]; + pRegion->offRegion = uLbaStart; + if (enmDataMode == CUEKEYWORD_MODE1_2352) + { + pRegion->cbBlock = 2352; + pRegion->enmDataForm = VDREGIONDATAFORM_MODE1_2352; + } + else if (enmDataMode == CUEKEYWORD_MODE2_2352) + { + pRegion->cbBlock = 2352; + pRegion->enmDataForm = VDREGIONDATAFORM_MODE2_2352; + } + else if (enmDataMode == CUEKEYWORD_AUDIO) + { + pRegion->cbBlock = 2352; + pRegion->enmDataForm = VDREGIONDATAFORM_CDDA; + } + else + { + pRegion->cbBlock = 2048; + pRegion->enmDataForm = VDREGIONDATAFORM_MODE1_2048; + } + pRegion->enmMetadataForm = VDREGIONMETADATAFORM_NONE; + pRegion->cbData = pRegion->cbBlock; + pRegion->cbMetadata = 0; + } + else + rc = vdIfError(pThis->pIfError, rc, RT_SRC_POS, + N_("CUE: Failed to allocate memory for the track list for '%s'"), + pThis->pszFilename); + } + } + else + rc = vdIfError(pThis->pIfError, VERR_NOT_SUPPORTED, RT_SRC_POS, + N_("CUE: Error parsing '%s', the data mode is not supported"), pThis->pszFilename); + } + else + rc = vdIfError(pThis->pIfError, VERR_NOT_SUPPORTED, RT_SRC_POS, + N_("CUE: Error parsing '%s', expected data mode"), pThis->pszFilename); + } + else + rc = vdIfError(pThis->pIfError, VERR_NOT_SUPPORTED, RT_SRC_POS, + N_("CUE: Error parsing '%s', track number must be between 01 and 99"), pThis->pszFilename); + } + else + rc = vdIfError(pThis->pIfError, VERR_NOT_SUPPORTED, RT_SRC_POS, + N_("CUE: Error parsing '%s', expected track number after TRACK directive"), pThis->pszFilename); + + return rc; +} + +/** + * Parses a list of tracks which must come after a FILE directive. + * + * @returns VBox status code. + * @param pThis The CUE image state. + * @param pTokenizer The tokenizer state. + */ +static int cueParseTrackList(PCUEIMAGE pThis, PCUETOKENIZER pTokenizer) +{ + int rc = VINF_SUCCESS; + + /* + * Sometimes there is a TITLE/PERFORMER/SONGWRITER directive before the start of the track list, + * skip and ignore those. + */ + while ( RT_SUCCESS(rc) + && ( cueTokenizerSkipIfIsKeywordEqual(pTokenizer, CUEKEYWORD_TITLE) + || cueTokenizerSkipIfIsKeywordEqual(pTokenizer, CUEKEYWORD_PERFORMER) + || cueTokenizerSkipIfIsKeywordEqual(pTokenizer, CUEKEYWORD_SONGWRITER))) + rc = cueParseAndSkipStringRemainder(pThis, pTokenizer, "TITLE/PERFORMER/SONGWRITER"); + + while ( RT_SUCCESS(rc) + && cueTokenizerSkipIfIsKeywordEqual(pTokenizer, CUEKEYWORD_TRACK)) + rc = cueParseTrack(pThis, pTokenizer); + + return rc; +} + +/** + * Parses the remainder of a FILE directive. + * + * @returns VBox status code. + * @param pThis The CUE image state. + * @param pTokenizer The tokenizer state. + */ +static int cueParseFile(PCUEIMAGE pThis, PCUETOKENIZER pTokenizer) +{ + int rc = VINF_SUCCESS; + + /* First must come a string constant followed by a keyword giving the file type. */ + if (cueTokenizerGetTokenType(pTokenizer) == CUETOKENTYPE_STRING) + { + rc = cueTokenizerConsumeStringDup(pTokenizer, &pThis->pszDataFilename); + if (RT_SUCCESS(rc)) + { + if (cueTokenizerGetTokenType(pTokenizer) == CUETOKENTYPE_KEYWORD) + { + if (cueTokenizerSkipIfIsKeywordEqual(pTokenizer, CUEKEYWORD_BINARY)) + { + pThis->fLittleEndian = true; + rc = cueParseTrackList(pThis, pTokenizer); + } + else if (cueTokenizerSkipIfIsKeywordEqual(pTokenizer, CUEKEYWORD_MOTOROLA)) + { + pThis->fLittleEndian = false; + rc = cueParseTrackList(pThis, pTokenizer); + } + else + rc = vdIfError(pThis->pIfError, VERR_NOT_SUPPORTED, RT_SRC_POS, + N_("CUE: Error parsing '%s', the file type is not supported (only BINARY)"), pThis->pszFilename); + } + else + rc = vdIfError(pThis->pIfError, rc, RT_SRC_POS, + N_("CUE: Error parsing '%s', expected file type"), pThis->pszFilename); + } + else + rc = vdIfError(pThis->pIfError, rc, RT_SRC_POS, + N_("CUE: Error parsing '%s', failed to allocate memory for filename"), pThis->pszFilename); + } + else + rc = vdIfError(pThis->pIfError, VERR_NOT_SUPPORTED, RT_SRC_POS, + N_("CUE: Error parsing '%s', expected filename after FILE directive"), pThis->pszFilename); + + return rc; +} + +/** + * Parses the keyword in the given tokenizer. + * + * @returns VBox status code. + * @param pThis The CUE image state. + * @param pTokenizer The tokenizer state. + */ +static int cueParseKeyword(PCUEIMAGE pThis, PCUETOKENIZER pTokenizer) +{ + int rc = VINF_SUCCESS; + + if (cueTokenizerSkipIfIsKeywordEqual(pTokenizer, CUEKEYWORD_FILE)) + rc = cueParseFile(pThis, pTokenizer); + else /* Skip all other keywords we don't need/support. */ + cueTokenizerConsume(pTokenizer); + + return rc; +} + + +/** + * Parses the CUE sheet from the given tokenizer. + * + * @returns VBox status code. + * @param pThis The CUE image state. + * @param pTokenizer The tokenizer state. + */ +static int cueParseFromTokenizer(PCUEIMAGE pThis, PCUETOKENIZER pTokenizer) +{ + int rc = VINF_SUCCESS; + + LogFlowFunc(("pThis=%p\n", pThis)); + + /* We don't support multiple FILE directives for now. */ + if (cueTokenizerGetTokenType(pTokenizer) == CUETOKENTYPE_KEYWORD) + rc = cueParseKeyword(pThis, pTokenizer); + else + rc = vdIfError(pThis->pIfError, VERR_NOT_SUPPORTED, RT_SRC_POS, + N_("CUE: Error parsing '%s', expected a keyword"), pThis->pszFilename); + + if ( RT_SUCCESS(rc) + && cueTokenizerGetTokenType(pTokenizer) != CUETOKENTYPE_EOS) + rc = vdIfError(pThis->pIfError, VERR_NOT_SUPPORTED, RT_SRC_POS, + N_("CUE: Error parsing '%s', expected end of stream"), pThis->pszFilename); + + LogFlowFunc(("returns rc=%Rrc\n", rc)); + return rc; +} + +/** + * Finalizes the track list of the image. + * + * @returns VBox status code. + * @param pThis The CUE image state. + * @param cbImage Size of the image data in bytes. + */ +static int cueTrackListFinalize(PCUEIMAGE pThis, uint64_t cbImage) +{ + int rc = VINF_SUCCESS; + + if ( pThis->cTracksMax == 0 + || pThis->pRegionList->aRegions[0].offRegion == UINT64_MAX) + return vdIfError(pThis->pIfError, VERR_NOT_SUPPORTED, RT_SRC_POS, + N_("CUE: Error parsing '%s', detected empty track list"), pThis->pszFilename); + + /* + * Fixup the track list to contain the proper sizes now that we parsed all tracks, + * check also that there are no gaps in the list. + */ + uint32_t cTracks = 1; + uint64_t offDisk = 0; + for (uint32_t i = 1; i < pThis->cTracksMax; i++) + { + PVDREGIONDESC pRegion = &pThis->pRegionList->aRegions[i]; + PVDREGIONDESC pRegionPrev = &pThis->pRegionList->aRegions[i - 1]; + if (pRegion->offRegion != UINT64_MAX) + { + cTracks++; + uint64_t cBlocks = pRegion->offRegion - (pRegionPrev->offRegion / pRegionPrev->cbBlock); + pRegionPrev->cRegionBlocksOrBytes = pRegionPrev->cbBlock * cBlocks; + offDisk += pRegionPrev->cRegionBlocksOrBytes; + + if (cbImage < pRegionPrev->cRegionBlocksOrBytes) + return vdIfError(pThis->pIfError, VERR_NOT_SUPPORTED, RT_SRC_POS, + N_("CUE: Error parsing '%s', image file is too small for track list"), + pThis->pszFilename); + + cbImage -= pRegionPrev->cRegionBlocksOrBytes; + pRegion->offRegion = offDisk; + } + else + break; + } + + /* Fixup last track. */ + PVDREGIONDESC pRegion = &pThis->pRegionList->aRegions[cTracks - 1]; + pRegion->cRegionBlocksOrBytes = cbImage; + + pThis->pRegionList->cRegions = cTracks; + pThis->pRegionList->fFlags = 0; + + /* Check that there are no gaps in the track list. */ + for (uint32_t i = cTracks; cTracks < pThis->cTracksMax; i++) + { + pRegion = &pThis->pRegionList->aRegions[i]; + if (pRegion->offRegion != UINT64_MAX) + { + rc = vdIfError(pThis->pIfError, VERR_NOT_SUPPORTED, RT_SRC_POS, + N_("CUE: Error parsing '%s', detected gaps in the track list"), pThis->pszFilename); + break; + } + } + + return rc; +} + +/** + * Internal. Free all allocated space for representing an image except pThis, + * and optionally delete the image from disk. + */ +static int cueFreeImage(PCUEIMAGE pThis, bool fDelete) +{ + int rc = VINF_SUCCESS; + + /* Freeing a never allocated image (e.g. because the open failed) is + * not signalled as an error. After all nothing bad happens. */ + if (pThis) + { + if (pThis->pStorage) + { + rc = vdIfIoIntFileClose(pThis->pIfIo, pThis->pStorage); + pThis->pStorage = NULL; + } + + if (pThis->pStorageData) + { + rc = vdIfIoIntFileClose(pThis->pIfIo, pThis->pStorageData); + pThis->pStorageData = NULL; + } + + if (pThis->pRegionList) + { + RTMemFree(pThis->pRegionList); + pThis->pRegionList = NULL; + } + + if (pThis->pszDataFilename) + { + RTStrFree(pThis->pszDataFilename); + pThis->pszDataFilename = NULL; + } + + if (fDelete && pThis->pszFilename) + vdIfIoIntFileDelete(pThis->pIfIo, pThis->pszFilename); + } + + LogFlowFunc(("returns %Rrc\n", rc)); + return rc; +} + +/** + * Internal: Open an image, constructing all necessary data structures. + */ +static int cueOpenImage(PCUEIMAGE pThis, unsigned uOpenFlags) +{ + pThis->uOpenFlags = uOpenFlags; + + pThis->pIfError = VDIfErrorGet(pThis->pVDIfsDisk); + pThis->pIfIo = VDIfIoIntGet(pThis->pVDIfsImage); + AssertPtrReturn(pThis->pIfIo, VERR_INVALID_PARAMETER); + + /* Open the image. */ + int rc = vdIfIoIntFileOpen(pThis->pIfIo, pThis->pszFilename, + VDOpenFlagsToFileOpenFlags(uOpenFlags, + false /* fCreate */), + &pThis->pStorage); + if (RT_SUCCESS(rc)) + { + uint64_t cbFile; + /* The descriptor file shouldn't be huge, so limit ourselfs to 16KB for now. */ + rc = vdIfIoIntFileGetSize(pThis->pIfIo, pThis->pStorage, &cbFile); + if ( RT_SUCCESS(rc) + && cbFile <= _16K - 1) + { + char szInput[_16K]; + RT_ZERO(szInput); + + rc = vdIfIoIntFileReadSync(pThis->pIfIo, pThis->pStorage, 0, + &szInput, cbFile); + if (RT_SUCCESS(rc)) + { + RTStrPurgeEncoding(&szInput[0]); + PCUETOKENIZER pTokenizer = cueTokenizerCreate(&szInput[0]); + if (pTokenizer) + { + rc = cueParseFromTokenizer(pThis, pTokenizer); + RTMemFree(pTokenizer); + if (RT_SUCCESS(rc)) + { + /* Open the backing file. */ + char szBackingFile[RTPATH_MAX]; + rc = RTStrCopy(&szBackingFile[0], sizeof(szBackingFile), pThis->pszFilename); + if (RT_SUCCESS(rc)) + { + RTPathStripFilename(&szBackingFile[0]); + rc = RTPathAppend(&szBackingFile[0], sizeof(szBackingFile), pThis->pszDataFilename); + if (RT_SUCCESS(rc)) + { + rc = vdIfIoIntFileOpen(pThis->pIfIo, szBackingFile, + VDOpenFlagsToFileOpenFlags(uOpenFlags, + false /* fCreate */), + &pThis->pStorageData); + if (RT_SUCCESS(rc)) + { + rc = vdIfIoIntFileGetSize(pThis->pIfIo, pThis->pStorageData, &cbFile); + if (RT_SUCCESS(rc)) + rc = cueTrackListFinalize(pThis, cbFile); + else + rc = vdIfError(pThis->pIfError, rc, RT_SRC_POS, + N_("CUE: Unable to query size of backing file '%s'"), + szBackingFile); + } + else + rc = vdIfError(pThis->pIfError, rc, RT_SRC_POS, + N_("CUE: Unable to open backing file '%s'"), + szBackingFile); + } + else + rc = vdIfError(pThis->pIfError, rc, RT_SRC_POS, + N_("CUE: Error constructing backing filename from '%s'"), + pThis->pszFilename); + } + else + rc = vdIfError(pThis->pIfError, rc, RT_SRC_POS, + N_("CUE: Error constructing backing filename from '%s'"), + pThis->pszFilename); + } + } + } + else + rc = vdIfError(pThis->pIfError, rc, RT_SRC_POS, N_("CUE: Error reading '%s'"), pThis->pszFilename); + } + else if (RT_SUCCESS(rc)) + rc = vdIfError(pThis->pIfError, VERR_VD_INVALID_SIZE, + RT_SRC_POS, N_("CUE: The descriptor file '%s' is too huge (%llu vs %llu)"), + pThis->pszFilename, cbFile, _16K - 1); + } + /* else: Do NOT signal an appropriate error here, as the VD layer has the + * choice of retrying the open if it failed. */ + + if (RT_FAILURE(rc)) + cueFreeImage(pThis, false); + return rc; +} + +/** + * Converts the data form enumeration to a string. + * + * @returns String name of the given data form. + * @param enmDataForm The data form. + */ +static const char *cueRegionDataFormStringify(VDREGIONDATAFORM enmDataForm) +{ + switch (enmDataForm) + { + #define DATAFORM2STR(tag) case VDREGIONDATAFORM_##tag: return #tag + + DATAFORM2STR(INVALID); + DATAFORM2STR(RAW); + DATAFORM2STR(CDDA); + DATAFORM2STR(CDDA_PAUSE); + DATAFORM2STR(MODE1_2048); + DATAFORM2STR(MODE1_2352); + DATAFORM2STR(MODE1_0); + DATAFORM2STR(XA_2336); + DATAFORM2STR(XA_2352); + DATAFORM2STR(XA_0); + DATAFORM2STR(MODE2_2336); + DATAFORM2STR(MODE2_2352); + DATAFORM2STR(MODE2_0); + + #undef DATAFORM2STR + + default: + { + AssertMsgFailed(("Unknown data form %d! forgot to add it to the switch?\n", enmDataForm)); + return "UNKNOWN!"; + } + } +} + +/** + * Converts the data form enumeration to a string. + * + * @returns String name of the given data form. + * @param enmMetadataForm The metadata form. + */ +static const char *cueRegionMetadataFormStringify(VDREGIONMETADATAFORM enmMetadataForm) +{ + switch (enmMetadataForm) + { + #define METADATAFORM2STR(tag) case VDREGIONMETADATAFORM_##tag: return #tag + + METADATAFORM2STR(INVALID); + METADATAFORM2STR(RAW); + METADATAFORM2STR(NONE); + + #undef METADATAFORM2STR + + default: + { + AssertMsgFailed(("Unknown metadata form %d! forgot to add it to the switch?\n", enmMetadataForm)); + return "UNKNOWN!"; + } + } +} + +/** + * Returns the region containing the given offset. + * + * @returns Pointer to the region or NULL if not found. + * @param pThis The CUE image state. + * @param uOffset The offset to look for. + */ +static PCVDREGIONDESC cueRegionQueryByOffset(PCUEIMAGE pThis, uint64_t uOffset) +{ + for (uint32_t i = 0; i < pThis->pRegionList->cRegions; i++) + { + PCVDREGIONDESC pRegion = &pThis->pRegionList->aRegions[i]; + if ( pRegion->offRegion <= uOffset + && pRegion->offRegion + pRegion->cRegionBlocksOrBytes > uOffset) + return pRegion; + } + + return NULL; +} + +/** @copydoc VDIMAGEBACKEND::pfnProbe */ +static DECLCALLBACK(int) cueProbe(const char *pszFilename, PVDINTERFACE pVDIfsDisk, + PVDINTERFACE pVDIfsImage, VDTYPE enmDesiredType, VDTYPE *penmType) +{ + RT_NOREF(pVDIfsDisk, enmDesiredType); + LogFlowFunc(("pszFilename=\"%s\" pVDIfsDisk=%#p pVDIfsImage=%#p\n", pszFilename, pVDIfsDisk, pVDIfsImage)); + int rc = VINF_SUCCESS; + + AssertPtrReturn(pszFilename, VERR_INVALID_POINTER); + AssertReturn(*pszFilename != '\0', VERR_INVALID_PARAMETER); + + + PCUEIMAGE pThis = (PCUEIMAGE)RTMemAllocZ(sizeof(CUEIMAGE)); + if (RT_LIKELY(pThis)) + { + pThis->pszFilename = pszFilename; + pThis->pStorage = NULL; + pThis->pVDIfsDisk = pVDIfsDisk; + pThis->pVDIfsImage = pVDIfsImage; + + rc = cueOpenImage(pThis, VD_OPEN_FLAGS_INFO | VD_OPEN_FLAGS_READONLY); + cueFreeImage(pThis, false); + RTMemFree(pThis); + + if (RT_SUCCESS(rc)) + *penmType = VDTYPE_OPTICAL_DISC; + else + rc = VERR_VD_GEN_INVALID_HEADER; + } + else + rc = VERR_NO_MEMORY; + + LogFlowFunc(("returns %Rrc\n", rc)); + return rc; +} + +/** @copydoc VDIMAGEBACKEND::pfnOpen */ +static DECLCALLBACK(int) cueOpen(const char *pszFilename, unsigned uOpenFlags, + PVDINTERFACE pVDIfsDisk, PVDINTERFACE pVDIfsImage, + VDTYPE enmType, void **ppBackendData) +{ + LogFlowFunc(("pszFilename=\"%s\" uOpenFlags=%#x pVDIfsDisk=%#p pVDIfsImage=%#p enmType=%u ppBackendData=%#p\n", + pszFilename, uOpenFlags, pVDIfsDisk, pVDIfsImage, enmType, ppBackendData)); + int rc; + PCUEIMAGE pThis; + + /* Check open flags. All valid flags are supported. */ + AssertReturn(!(uOpenFlags & ~VD_OPEN_FLAGS_MASK), VERR_INVALID_PARAMETER); + AssertPtrReturn(pszFilename, VERR_INVALID_POINTER); + AssertReturn(*pszFilename != '\0', VERR_INVALID_PARAMETER); + + AssertReturn(enmType == VDTYPE_OPTICAL_DISC, VERR_NOT_SUPPORTED); + + pThis = (PCUEIMAGE)RTMemAllocZ(sizeof(CUEIMAGE)); + if (RT_LIKELY(pThis)) + { + pThis->pszFilename = pszFilename; + pThis->pStorage = NULL; + pThis->pVDIfsDisk = pVDIfsDisk; + pThis->pVDIfsImage = pVDIfsImage; + + rc = cueOpenImage(pThis, uOpenFlags); + if (RT_SUCCESS(rc)) + *ppBackendData = pThis; + else + RTMemFree(pThis); + } + else + rc = VERR_NO_MEMORY; + + LogFlowFunc(("returns %Rrc (pBackendData=%#p)\n", rc, *ppBackendData)); + return rc; +} + +/** @copydoc VDIMAGEBACKEND::pfnClose */ +static DECLCALLBACK(int) cueClose(void *pBackendData, bool fDelete) +{ + LogFlowFunc(("pBackendData=%#p fDelete=%d\n", pBackendData, fDelete)); + PCUEIMAGE pThis = (PCUEIMAGE)pBackendData; + int rc = cueFreeImage(pThis, fDelete); + RTMemFree(pThis); + + LogFlowFunc(("returns %Rrc\n", rc)); + return rc; +} + +/** @copydoc VDIMAGEBACKEND::pfnRead */ +static DECLCALLBACK(int) cueRead(void *pBackendData, uint64_t uOffset, size_t cbToRead, + PVDIOCTX pIoCtx, size_t *pcbActuallyRead) +{ + LogFlowFunc(("pBackendData=%#p uOffset=%llu cbToRead=%zu pIoCtx=%#p pcbActuallyRead=%#p\n", + pBackendData, uOffset, cbToRead, pIoCtx, pcbActuallyRead)); + int rc = VINF_SUCCESS; + PCUEIMAGE pThis = (PCUEIMAGE)pBackendData; + + /* Get the region */ + PCVDREGIONDESC pRegion = cueRegionQueryByOffset(pThis, uOffset); + if (pRegion) + { + /* Clip read size to remain in the region (not necessary I think). */ + uint64_t offRead = uOffset - pRegion->offRegion; + + cbToRead = RT_MIN(cbToRead, pRegion->cRegionBlocksOrBytes - offRead); + Assert(!(cbToRead % pRegion->cbBlock)); + + /* Need to convert audio data samples to little endian. */ + if ( pRegion->enmDataForm == VDREGIONDATAFORM_CDDA + && !pThis->fLittleEndian) + { + *pcbActuallyRead = cbToRead; + + while (cbToRead) + { + RTSGSEG Segment; + unsigned cSegments = 1; + size_t cbSeg = 0; + + cbSeg = vdIfIoIntIoCtxSegArrayCreate(pThis->pIfIo, pIoCtx, &Segment, + &cSegments, cbToRead); + + rc = vdIfIoIntFileReadSync(pThis->pIfIo, pThis->pStorageData, uOffset, Segment.pvSeg, cbSeg); + if (RT_FAILURE(rc)) + break; + + uint16_t *pu16Buf = (uint16_t *)Segment.pvSeg; + for (uint32_t i = 0; i < cbSeg / sizeof(uint16_t); i++) + { + *pu16Buf = RT_BSWAP_U16(*pu16Buf); + pu16Buf++; + } + + cbToRead -= RT_MIN(cbToRead, cbSeg); + uOffset += cbSeg; + } + } + else + { + rc = vdIfIoIntFileReadUser(pThis->pIfIo, pThis->pStorageData, uOffset, + pIoCtx, cbToRead); + if (RT_SUCCESS(rc)) + *pcbActuallyRead = cbToRead; + } + } + else + rc = VERR_INVALID_PARAMETER; + + return rc; +} + +/** @copydoc VDIMAGEBACKEND::pfnWrite */ +static DECLCALLBACK(int) cueWrite(void *pBackendData, uint64_t uOffset, size_t cbToWrite, + PVDIOCTX pIoCtx, size_t *pcbWriteProcess, size_t *pcbPreRead, + size_t *pcbPostRead, unsigned fWrite) +{ + RT_NOREF7(uOffset, cbToWrite, pIoCtx, pcbWriteProcess, pcbPreRead, pcbPostRead, fWrite); + LogFlowFunc(("pBackendData=%#p uOffset=%llu pIoCtx=%#p cbToWrite=%zu pcbWriteProcess=%#p pcbPreRead=%#p pcbPostRead=%#p\n", + pBackendData, uOffset, pIoCtx, cbToWrite, pcbWriteProcess, pcbPreRead, pcbPostRead)); + PCUEIMAGE pThis = (PCUEIMAGE)pBackendData; + int rc; + + AssertPtr(pThis); + + if (pThis->uOpenFlags & VD_OPEN_FLAGS_READONLY) + rc = VERR_VD_IMAGE_READ_ONLY; + else + rc = VERR_NOT_SUPPORTED; + + LogFlowFunc(("returns %Rrc\n", rc)); + return rc; +} + +/** @copydoc VDIMAGEBACKEND::pfnFlush */ +static DECLCALLBACK(int) cueFlush(void *pBackendData, PVDIOCTX pIoCtx) +{ + RT_NOREF2(pBackendData, pIoCtx); + + return VINF_SUCCESS; +} + +/** @copydoc VDIMAGEBACKEND::pfnGetVersion */ +static DECLCALLBACK(unsigned) cueGetVersion(void *pBackendData) +{ + LogFlowFunc(("pBackendData=%#p\n", pBackendData)); + PCUEIMAGE pThis = (PCUEIMAGE)pBackendData; + + AssertPtrReturn(pThis, 0); + + return 1; +} + +/** @copydoc VDIMAGEBACKEND::pfnGetFileSize */ +static DECLCALLBACK(uint64_t) cueGetFileSize(void *pBackendData) +{ + LogFlowFunc(("pBackendData=%#p\n", pBackendData)); + PCUEIMAGE pThis = (PCUEIMAGE)pBackendData; + + AssertPtrReturn(pThis, 0); + + uint64_t cbFile = 0; + if (pThis->pStorage) + { + int rc = vdIfIoIntFileGetSize(pThis->pIfIo, pThis->pStorageData, &cbFile); + if (RT_FAILURE(rc)) + cbFile = 0; /* Make sure it is 0 */ + } + + LogFlowFunc(("returns %lld\n", cbFile)); + return cbFile; +} + +/** @copydoc VDIMAGEBACKEND::pfnGetPCHSGeometry */ +static DECLCALLBACK(int) cueGetPCHSGeometry(void *pBackendData, + PVDGEOMETRY pPCHSGeometry) +{ + RT_NOREF1(pPCHSGeometry); + LogFlowFunc(("pBackendData=%#p pPCHSGeometry=%#p\n", pBackendData, pPCHSGeometry)); + PCUEIMAGE pThis = (PCUEIMAGE)pBackendData; + int rc = VINF_SUCCESS; + + AssertPtrReturn(pThis, VERR_VD_NOT_OPENED); + + rc = VERR_NOT_SUPPORTED; + + LogFlowFunc(("returns %Rrc (PCHS=%u/%u/%u)\n", rc, pPCHSGeometry->cCylinders, pPCHSGeometry->cHeads, pPCHSGeometry->cSectors)); + return rc; +} + +/** @copydoc VDIMAGEBACKEND::pfnSetPCHSGeometry */ +static DECLCALLBACK(int) cueSetPCHSGeometry(void *pBackendData, + PCVDGEOMETRY pPCHSGeometry) +{ + RT_NOREF1(pPCHSGeometry); + LogFlowFunc(("pBackendData=%#p pPCHSGeometry=%#p PCHS=%u/%u/%u\n", + pBackendData, pPCHSGeometry, pPCHSGeometry->cCylinders, pPCHSGeometry->cHeads, pPCHSGeometry->cSectors)); + PCUEIMAGE pThis = (PCUEIMAGE)pBackendData; + int rc = VINF_SUCCESS; + + AssertPtrReturn(pThis, VERR_VD_NOT_OPENED); + + if (pThis->uOpenFlags & VD_OPEN_FLAGS_READONLY) + rc = VERR_VD_IMAGE_READ_ONLY; + else + rc = VERR_NOT_SUPPORTED; + + LogFlowFunc(("returns %Rrc\n", rc)); + return rc; +} + +/** @copydoc VDIMAGEBACKEND::pfnGetLCHSGeometry */ +static DECLCALLBACK(int) cueGetLCHSGeometry(void *pBackendData, + PVDGEOMETRY pLCHSGeometry) +{ + RT_NOREF1(pLCHSGeometry); + LogFlowFunc(("pBackendData=%#p pLCHSGeometry=%#p\n", pBackendData, pLCHSGeometry)); + PCUEIMAGE pThis = (PCUEIMAGE)pBackendData; + int rc = VINF_SUCCESS; + + AssertPtrReturn(pThis, VERR_VD_NOT_OPENED); + + rc = VERR_NOT_SUPPORTED; + + LogFlowFunc(("returns %Rrc (LCHS=%u/%u/%u)\n", rc, pLCHSGeometry->cCylinders, pLCHSGeometry->cHeads, pLCHSGeometry->cSectors)); + return rc; +} + +/** @copydoc VDIMAGEBACKEND::pfnSetLCHSGeometry */ +static DECLCALLBACK(int) cueSetLCHSGeometry(void *pBackendData, + PCVDGEOMETRY pLCHSGeometry) +{ + RT_NOREF1(pLCHSGeometry); + LogFlowFunc(("pBackendData=%#p pLCHSGeometry=%#p LCHS=%u/%u/%u\n", + pBackendData, pLCHSGeometry, pLCHSGeometry->cCylinders, pLCHSGeometry->cHeads, pLCHSGeometry->cSectors)); + PCUEIMAGE pThis = (PCUEIMAGE)pBackendData; + int rc = VINF_SUCCESS; + + AssertPtrReturn(pThis, VERR_VD_NOT_OPENED); + + if (pThis->uOpenFlags & VD_OPEN_FLAGS_READONLY) + rc = VERR_VD_IMAGE_READ_ONLY; + else + rc = VERR_NOT_SUPPORTED; + + LogFlowFunc(("returns %Rrc\n", rc)); + return rc; +} + +/** @copydoc VDIMAGEBACKEND::pfnQueryRegions */ +static DECLCALLBACK(int) cueQueryRegions(void *pBackendData, PCVDREGIONLIST *ppRegionList) +{ + LogFlowFunc(("pBackendData=%#p ppRegionList=%#p\n", pBackendData, ppRegionList)); + PCUEIMAGE pThis = (PCUEIMAGE)pBackendData; + + AssertPtrReturn(pThis, VERR_VD_NOT_OPENED); + + *ppRegionList = pThis->pRegionList; + LogFlowFunc(("returns %Rrc\n", VINF_SUCCESS)); + return VINF_SUCCESS; +} + +/** @copydoc VDIMAGEBACKEND::pfnRegionListRelease */ +static DECLCALLBACK(void) cueRegionListRelease(void *pBackendData, PCVDREGIONLIST pRegionList) +{ + RT_NOREF1(pRegionList); + LogFlowFunc(("pBackendData=%#p pRegionList=%#p\n", pBackendData, pRegionList)); + PCUEIMAGE pThis = (PCUEIMAGE)pBackendData; + AssertPtr(pThis); RT_NOREF(pThis); + + /* Nothing to do here. */ +} + +/** @copydoc VDIMAGEBACKEND::pfnGetImageFlags */ +static DECLCALLBACK(unsigned) cueGetImageFlags(void *pBackendData) +{ + LogFlowFunc(("pBackendData=%#p\n", pBackendData)); + PCUEIMAGE pThis = (PCUEIMAGE)pBackendData; + + AssertPtrReturn(pThis, 0); + + LogFlowFunc(("returns %#x\n", pThis->uImageFlags)); + return pThis->uImageFlags; +} + +/** @copydoc VDIMAGEBACKEND::pfnGetOpenFlags */ +static DECLCALLBACK(unsigned) cueGetOpenFlags(void *pBackendData) +{ + LogFlowFunc(("pBackendData=%#p\n", pBackendData)); + PCUEIMAGE pThis = (PCUEIMAGE)pBackendData; + + AssertPtrReturn(pThis, 0); + + LogFlowFunc(("returns %#x\n", pThis->uOpenFlags)); + return pThis->uOpenFlags; +} + +/** @copydoc VDIMAGEBACKEND::pfnSetOpenFlags */ +static DECLCALLBACK(int) cueSetOpenFlags(void *pBackendData, unsigned uOpenFlags) +{ + LogFlowFunc(("pBackendData=%#p\n uOpenFlags=%#x", pBackendData, uOpenFlags)); + PCUEIMAGE pThis = (PCUEIMAGE)pBackendData; + int rc = VINF_SUCCESS; + + /* Image must be opened and the new flags must be valid. */ + if (!pThis || (uOpenFlags & ~( VD_OPEN_FLAGS_READONLY | VD_OPEN_FLAGS_INFO + | VD_OPEN_FLAGS_ASYNC_IO | VD_OPEN_FLAGS_SHAREABLE + | VD_OPEN_FLAGS_SEQUENTIAL | VD_OPEN_FLAGS_SKIP_CONSISTENCY_CHECKS))) + rc = VERR_INVALID_PARAMETER; + else + { + /* Implement this operation via reopening the image. */ + rc = cueFreeImage(pThis, false); + if (RT_SUCCESS(rc)) + rc = cueOpenImage(pThis, uOpenFlags); + } + + LogFlowFunc(("returns %Rrc\n", rc)); + return rc; +} + +/** @copydoc VDIMAGEBACKEND::pfnGetComment */ +VD_BACKEND_CALLBACK_GET_COMMENT_DEF_NOT_SUPPORTED(cueGetComment); + +/** @copydoc VDIMAGEBACKEND::pfnSetComment */ +VD_BACKEND_CALLBACK_SET_COMMENT_DEF_NOT_SUPPORTED(cueSetComment, PCUEIMAGE); + +/** @copydoc VDIMAGEBACKEND::pfnGetUuid */ +VD_BACKEND_CALLBACK_GET_UUID_DEF_NOT_SUPPORTED(cueGetUuid); + +/** @copydoc VDIMAGEBACKEND::pfnSetUuid */ +VD_BACKEND_CALLBACK_SET_UUID_DEF_NOT_SUPPORTED(cueSetUuid, PCUEIMAGE); + +/** @copydoc VDIMAGEBACKEND::pfnGetModificationUuid */ +VD_BACKEND_CALLBACK_GET_UUID_DEF_NOT_SUPPORTED(cueGetModificationUuid); + +/** @copydoc VDIMAGEBACKEND::pfnSetModificationUuid */ +VD_BACKEND_CALLBACK_SET_UUID_DEF_NOT_SUPPORTED(cueSetModificationUuid, PCUEIMAGE); + +/** @copydoc VDIMAGEBACKEND::pfnGetParentUuid */ +VD_BACKEND_CALLBACK_GET_UUID_DEF_NOT_SUPPORTED(cueGetParentUuid); + +/** @copydoc VDIMAGEBACKEND::pfnSetParentUuid */ +VD_BACKEND_CALLBACK_SET_UUID_DEF_NOT_SUPPORTED(cueSetParentUuid, PCUEIMAGE); + +/** @copydoc VDIMAGEBACKEND::pfnGetParentModificationUuid */ +VD_BACKEND_CALLBACK_GET_UUID_DEF_NOT_SUPPORTED(cueGetParentModificationUuid); + +/** @copydoc VDIMAGEBACKEND::pfnSetParentModificationUuid */ +VD_BACKEND_CALLBACK_SET_UUID_DEF_NOT_SUPPORTED(cueSetParentModificationUuid, PCUEIMAGE); + +/** @copydoc VDIMAGEBACKEND::pfnDump */ +static DECLCALLBACK(void) cueDump(void *pBackendData) +{ + PCUEIMAGE pThis = (PCUEIMAGE)pBackendData; + + AssertPtrReturnVoid(pThis); + vdIfErrorMessage(pThis->pIfError, "Dumping CUE image \"%s\" mode=%s uOpenFlags=%X File=%#p\n", + pThis->pszFilename, + (pThis->uOpenFlags & VD_OPEN_FLAGS_READONLY) ? "r/o" : "r/w", + pThis->uOpenFlags, + pThis->pStorage); + vdIfErrorMessage(pThis->pIfError, "Backing File \"%s\" File=%#p\n", + pThis->pszDataFilename, pThis->pStorageData); + vdIfErrorMessage(pThis->pIfError, "Number of tracks: %u\n", pThis->pRegionList->cRegions); + for (uint32_t i = 0; i < pThis->pRegionList->cRegions; i++) + { + PCVDREGIONDESC pRegion = &pThis->pRegionList->aRegions[i]; + + vdIfErrorMessage(pThis->pIfError, "------------------------ Track %u ------------------------\n", i); + vdIfErrorMessage(pThis->pIfError, "Start=%llu Size=%llu BlockSize=%llu DataSize=%llu MetadataSize=%llu\n", + pRegion->offRegion, pRegion->cRegionBlocksOrBytes, pRegion->cbBlock, pRegion->cbData, + pRegion->cbMetadata); + vdIfErrorMessage(pThis->pIfError, "DataForm=%s MetadataForm=%s\n", + cueRegionDataFormStringify(pRegion->enmDataForm), + cueRegionMetadataFormStringify(pRegion->enmMetadataForm)); + } +} + + + +const VDIMAGEBACKEND g_CueBackend = +{ + /* u32Version */ + VD_IMGBACKEND_VERSION, + /* pszBackendName */ + "CUE", + /* uBackendCaps */ + VD_CAP_FILE | VD_CAP_VFS, + /* paFileExtensions */ + s_aCueFileExtensions, + /* paConfigInfo */ + NULL, + /* pfnProbe */ + cueProbe, + /* pfnOpen */ + cueOpen, + /* pfnCreate */ + NULL, + /* pfnRename */ + NULL, + /* pfnClose */ + cueClose, + /* pfnRead */ + cueRead, + /* pfnWrite */ + cueWrite, + /* pfnFlush */ + cueFlush, + /* pfnDiscard */ + NULL, + /* pfnGetVersion */ + cueGetVersion, + /* pfnGetFileSize */ + cueGetFileSize, + /* pfnGetPCHSGeometry */ + cueGetPCHSGeometry, + /* pfnSetPCHSGeometry */ + cueSetPCHSGeometry, + /* pfnGetLCHSGeometry */ + cueGetLCHSGeometry, + /* pfnSetLCHSGeometry */ + cueSetLCHSGeometry, + /* pfnQueryRegions */ + cueQueryRegions, + /* pfnRegionListRelease */ + cueRegionListRelease, + /* pfnGetImageFlags */ + cueGetImageFlags, + /* pfnGetOpenFlags */ + cueGetOpenFlags, + /* pfnSetOpenFlags */ + cueSetOpenFlags, + /* pfnGetComment */ + cueGetComment, + /* pfnSetComment */ + cueSetComment, + /* pfnGetUuid */ + cueGetUuid, + /* pfnSetUuid */ + cueSetUuid, + /* pfnGetModificationUuid */ + cueGetModificationUuid, + /* pfnSetModificationUuid */ + cueSetModificationUuid, + /* pfnGetParentUuid */ + cueGetParentUuid, + /* pfnSetParentUuid */ + cueSetParentUuid, + /* pfnGetParentModificationUuid */ + cueGetParentModificationUuid, + /* pfnSetParentModificationUuid */ + cueSetParentModificationUuid, + /* pfnDump */ + cueDump, + /* pfnGetTimestamp */ + NULL, + /* pfnGetParentTimestamp */ + NULL, + /* pfnSetParentTimestamp */ + NULL, + /* pfnGetParentFilename */ + NULL, + /* pfnSetParentFilename */ + NULL, + /* pfnComposeLocation */ + genericFileComposeLocation, + /* pfnComposeName */ + genericFileComposeName, + /* pfnCompact */ + NULL, + /* pfnResize */ + NULL, + /* pfnRepair */ + NULL, + /* pfnTraverseMetadata */ + NULL, + /* u32VersionEnd */ + VD_IMGBACKEND_VERSION +}; |