353 lines
7.8 KiB
C++
353 lines
7.8 KiB
C++
|
#include "tag.h"
|
||
|
#include "header.h"
|
||
|
#include "flags.h"
|
||
|
#include "nu/ns_wc.h"
|
||
|
#include <limits.h>
|
||
|
#include <strsafe.h>
|
||
|
#include <stdint.h>
|
||
|
|
||
|
/*
|
||
|
http://wiki.hydrogenaudio.org/index.php?title=APEv2_specification
|
||
|
*/
|
||
|
|
||
|
APEv2::Tag::Tag()
|
||
|
{
|
||
|
flags = 0; // default to writing just a footer
|
||
|
}
|
||
|
|
||
|
int APEv2::Tag::Parse(const void *_data, size_t len)
|
||
|
{
|
||
|
char *data = (char *)_data;
|
||
|
|
||
|
if (len < Header::SIZE)
|
||
|
return APEV2_TOO_SMALL;
|
||
|
|
||
|
char *headerStart = data+(len-Header::SIZE);
|
||
|
|
||
|
Header footer(headerStart);
|
||
|
if (footer.Valid()
|
||
|
&& !(len == Header::SIZE && footer.IsHeader())) // if all we have is this footer, we should check that it's not really a header
|
||
|
{
|
||
|
len -= Header::SIZE;
|
||
|
char *dataStart = data;
|
||
|
if (footer.HasHeader())
|
||
|
{
|
||
|
// TODO: validate header
|
||
|
dataStart+= Header::SIZE;
|
||
|
len-=Header::SIZE;
|
||
|
}
|
||
|
flags = footer.GetFlags();
|
||
|
flags &= ~FLAG_HEADER_NO_FOOTER; // winamp 5.54 had this flag reversed, so let's correct it
|
||
|
return ParseData(dataStart, len);
|
||
|
}
|
||
|
|
||
|
// ok, we didn't have a footer, so let's see if we have a header
|
||
|
headerStart = data;
|
||
|
Header header(headerStart);
|
||
|
if (header.Valid())
|
||
|
{
|
||
|
len -= Header::SIZE;
|
||
|
char *dataStart = data + Header::SIZE;
|
||
|
if (header.HasFooter())
|
||
|
{
|
||
|
// TODO: validate footer
|
||
|
// benski> cut... we got here because we didn't have a footer.
|
||
|
//len-=Header::SIZE;
|
||
|
}
|
||
|
flags = header.GetFlags();
|
||
|
flags |= FLAG_HEADER_NO_FOOTER; // winamp 5.54 had this flag reversed, so let's correct it
|
||
|
return ParseData(dataStart, len);
|
||
|
}
|
||
|
|
||
|
return APEV2_FAILURE;
|
||
|
}
|
||
|
|
||
|
int APEv2::Tag::ParseData(const void *data, size_t len)
|
||
|
{
|
||
|
char *dataStart = (char *)data;
|
||
|
while (len)
|
||
|
{
|
||
|
Item *item = new Item;
|
||
|
size_t new_len = 0;
|
||
|
void *new_data = 0;
|
||
|
int ret = item->Read(dataStart, len, &new_data, &new_len);
|
||
|
if (ret == APEV2_SUCCESS)
|
||
|
{
|
||
|
len = new_len;
|
||
|
dataStart = (char *)new_data;
|
||
|
items.push_back(item);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
item->Release();
|
||
|
return ret;
|
||
|
}
|
||
|
}
|
||
|
return APEV2_SUCCESS;
|
||
|
}
|
||
|
|
||
|
int APEv2::Tag::GetString(const char *tag, wchar_t *data, size_t dataLen)
|
||
|
{
|
||
|
/* our UTF-8 to wchar_t conversion function wants an int for size, so we should explicitly check the size */
|
||
|
if (dataLen > INT_MAX)
|
||
|
return APEV2_FAILURE;
|
||
|
|
||
|
for ( APEv2::Item *l_item : items )
|
||
|
{
|
||
|
/* check if it's a string first, and then match the key next (will be faster) */
|
||
|
if ( l_item->IsString() && l_item->KeyMatch(tag, ITEM_KEY_COMPARE_CASE_INSENSITIVE))
|
||
|
{
|
||
|
void *item_data = 0;
|
||
|
size_t item_dataLen = 0;
|
||
|
if ( l_item->Get(&item_data, &item_dataLen) == APEV2_SUCCESS && item_dataLen < INT_MAX)
|
||
|
{
|
||
|
int signed_len = static_cast<int>(item_dataLen); // we checked against INT_MAX above so this conversion is safe
|
||
|
if (MultiByteToWideCharSZ(CP_UTF8, 0, (LPCSTR)item_data, signed_len, data, (int)dataLen) != 0)
|
||
|
return APEV2_SUCCESS;
|
||
|
}
|
||
|
return APEV2_NO_DATA;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return APEV2_KEY_NOT_FOUND;
|
||
|
}
|
||
|
|
||
|
int APEv2::Tag::SetItemData(APEv2::Item *item, const wchar_t *data)
|
||
|
{
|
||
|
int utf16_data_cch = (int)wcslen(data);
|
||
|
int item_dataLen = WideCharToMultiByte(CP_UTF8, 0, data, utf16_data_cch, 0, 0, NULL, NULL);
|
||
|
if (!item_dataLen)
|
||
|
return APEV2_FAILURE;
|
||
|
|
||
|
char *item_data = (char *)calloc(item_dataLen, sizeof(char));
|
||
|
if (item_data)
|
||
|
{
|
||
|
if (!WideCharToMultiByte(CP_UTF8, 0, data, utf16_data_cch, item_data, item_dataLen, NULL, NULL))
|
||
|
{
|
||
|
free(item_data);
|
||
|
return APEV2_FAILURE;
|
||
|
}
|
||
|
|
||
|
item->Set(item_data, item_dataLen, FLAG_ITEM_TEXT);
|
||
|
free(item_data);
|
||
|
return APEV2_SUCCESS;
|
||
|
}
|
||
|
return APEV2_FAILURE;
|
||
|
}
|
||
|
|
||
|
int APEv2::Tag::SetString(const char *tag, const wchar_t *data)
|
||
|
{
|
||
|
for (auto it = items.begin(); it != items.end(); it++)
|
||
|
{
|
||
|
APEv2::Item* item = *it;
|
||
|
if (item->KeyMatch(tag, ITEM_KEY_COMPARE_CASE_INSENSITIVE))
|
||
|
{
|
||
|
if (data && *data)
|
||
|
{
|
||
|
return SetItemData(item, data);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
item->Release();
|
||
|
it = items.erase(it);
|
||
|
return APEV2_SUCCESS;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (data && *data)
|
||
|
{
|
||
|
/* Not already in the list, so we need to add it */
|
||
|
Item *newItem = new Item;
|
||
|
|
||
|
if (newItem->SetKey(tag) == APEV2_SUCCESS && SetItemData(newItem, data) == APEV2_SUCCESS)
|
||
|
{
|
||
|
items.push_back(newItem);
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
newItem->Release();
|
||
|
return APEV2_FAILURE;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return APEV2_SUCCESS;
|
||
|
}
|
||
|
|
||
|
int APEv2::Tag::EnumValue(size_t i, const char **tag, wchar_t *data, size_t dataLen)
|
||
|
{
|
||
|
/* our UTF-8 to wchar_t conversion function wants an int for size, so we should explicitly check the size */
|
||
|
if (dataLen > INT_MAX)
|
||
|
return APEV2_FAILURE;
|
||
|
|
||
|
if (i >= items.size())
|
||
|
return APEV2_END_OF_ITEMS;
|
||
|
|
||
|
APEv2::Item *item = items[i];
|
||
|
item->GetKey(tag);
|
||
|
|
||
|
if (!data || !dataLen) // if they don't want the data, go ahead and return now
|
||
|
return APEV2_SUCCESS;
|
||
|
|
||
|
data[0]=0;
|
||
|
void *item_data = 0;
|
||
|
size_t item_dataLen = 0;
|
||
|
if (item->IsString())
|
||
|
{
|
||
|
if (item->Get(&item_data, &item_dataLen) == APEV2_SUCCESS && item_dataLen < INT_MAX)
|
||
|
{
|
||
|
int signed_len = static_cast<int>(item_dataLen); // we checked against INT_MAX above so this conversion is safe
|
||
|
if (MultiByteToWideCharSZ(CP_UTF8, 0, (LPCSTR)item_data, signed_len, data, (int)dataLen) == 0)
|
||
|
*data=0;
|
||
|
}
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// TODO: benski> hmmm
|
||
|
StringCchCopyW(data, dataLen, L"[TODO: deal with binary]");
|
||
|
}
|
||
|
|
||
|
return APEV2_SUCCESS;
|
||
|
}
|
||
|
|
||
|
void APEv2::Tag::Clear()
|
||
|
{
|
||
|
for ( APEv2::Item *l_item : items )
|
||
|
{
|
||
|
l_item->Release();
|
||
|
}
|
||
|
|
||
|
items.clear();
|
||
|
}
|
||
|
|
||
|
int APEv2::Tag::RemoveItem(size_t index)
|
||
|
{
|
||
|
if (index >= items.size())
|
||
|
return APEV2_END_OF_ITEMS;
|
||
|
|
||
|
APEv2::Item *item = items[index];
|
||
|
items.erase(items.begin() + index);
|
||
|
item->Release();
|
||
|
return APEV2_SUCCESS;
|
||
|
}
|
||
|
|
||
|
size_t APEv2::Tag::EncodeSize()
|
||
|
{
|
||
|
size_t totalSize=0;
|
||
|
|
||
|
if (flags & FLAG_HEADER_HAS_HEADER)
|
||
|
totalSize+=Header::SIZE;
|
||
|
|
||
|
for ( APEv2::Item *l_item : items )
|
||
|
{
|
||
|
totalSize += l_item->EncodeSize();
|
||
|
}
|
||
|
|
||
|
if (!(flags & FLAG_HEADER_NO_FOOTER))
|
||
|
totalSize+=Header::SIZE;
|
||
|
|
||
|
return totalSize;
|
||
|
}
|
||
|
|
||
|
int APEv2::Tag::Encode(void *data, size_t len)
|
||
|
{
|
||
|
size_t totalSize=0;
|
||
|
int8_t *ptr = (int8_t *)data;
|
||
|
|
||
|
if (flags & FLAG_HEADER_HAS_HEADER)
|
||
|
{
|
||
|
HeaderData headerData = {0};
|
||
|
headerData.size= (uint32_t)totalSize;
|
||
|
if (!(flags & FLAG_HEADER_NO_FOOTER))
|
||
|
headerData.size += Header::SIZE;
|
||
|
headerData.items = (uint32_t)items.size();
|
||
|
|
||
|
headerData.flags = (flags & FLAG_HEADER_ENCODE_MASK)|FLAG_HEADER_IS_HEADER;
|
||
|
|
||
|
Header header(&headerData);
|
||
|
int ret = header.Encode(ptr, len);
|
||
|
if (ret != APEV2_SUCCESS)
|
||
|
return ret;
|
||
|
ptr += Header::SIZE;
|
||
|
len -= Header::SIZE;
|
||
|
}
|
||
|
|
||
|
for ( APEv2::Item *l_item : items )
|
||
|
{
|
||
|
int ret = l_item->Encode(ptr, len);
|
||
|
if (ret!= APEV2_SUCCESS)
|
||
|
return ret;
|
||
|
size_t itemSize = l_item->EncodeSize();
|
||
|
len-=itemSize;
|
||
|
ptr+=itemSize;
|
||
|
totalSize+=itemSize;
|
||
|
}
|
||
|
|
||
|
if (!(flags & FLAG_HEADER_NO_FOOTER))
|
||
|
{
|
||
|
HeaderData footerData = {0};
|
||
|
footerData.size= (uint32_t)totalSize + Header::SIZE;
|
||
|
footerData.items = (uint32_t)items.size();
|
||
|
footerData.flags = (flags & FLAG_HEADER_ENCODE_MASK);
|
||
|
|
||
|
Header footer(&footerData);
|
||
|
int ret = footer.Encode(ptr, len);
|
||
|
if (ret != APEV2_SUCCESS)
|
||
|
return ret;
|
||
|
}
|
||
|
return APEV2_SUCCESS;
|
||
|
}
|
||
|
|
||
|
int APEv2::Tag::SetKeyValueByIndex(size_t index, const char *key, const wchar_t *data)
|
||
|
{
|
||
|
if (index >= items.size())
|
||
|
return APEV2_END_OF_ITEMS;
|
||
|
|
||
|
// TODO: check for duplicate key
|
||
|
|
||
|
items[index]->SetKey(key);
|
||
|
return SetItemData(items[index], data);
|
||
|
}
|
||
|
|
||
|
APEv2::Item *APEv2::Tag::AddItem()
|
||
|
{
|
||
|
Item *newItem = new Item;
|
||
|
items.push_back(newItem);
|
||
|
return newItem;
|
||
|
}
|
||
|
|
||
|
int APEv2::Tag::SetFlags(uint32_t newflags, uint32_t mask)
|
||
|
{
|
||
|
flags = (flags & ~mask) | newflags;
|
||
|
return APEV2_SUCCESS;
|
||
|
}
|
||
|
|
||
|
int APEv2::Tag::FindItemByKey(const char *key, size_t *index, int compare)
|
||
|
{
|
||
|
for (size_t i=0;i!=items.size();i++)
|
||
|
{
|
||
|
if (items[i]->KeyMatch(key, compare))
|
||
|
{
|
||
|
*index = i;
|
||
|
return APEV2_SUCCESS;
|
||
|
}
|
||
|
}
|
||
|
return APEV2_KEY_NOT_FOUND;
|
||
|
}
|
||
|
|
||
|
size_t APEv2::Tag::GetNumItems()
|
||
|
{
|
||
|
return items.size();
|
||
|
}
|
||
|
|
||
|
bool APEv2::Tag::IsReadOnly()
|
||
|
{
|
||
|
return flags & FLAG_READONLY;
|
||
|
}
|
||
|
|
||
|
bool APEv2::Tag::IsItemReadOnly(size_t index)
|
||
|
{
|
||
|
if (index >= items.size())
|
||
|
return true;
|
||
|
return items[index]->IsReadOnly();
|
||
|
}
|