Blob Blame History Raw
// =================================================================================================
// ADOBE SYSTEMS INCORPORATED
// Copyright 2008 Adobe Systems Incorporated
// All Rights Reserved
//
// NOTICE: Adobe permits you to use, modify, and distribute this file in accordance with the terms
// of the Adobe license agreement accompanying it.
// =================================================================================================

#include "public/include/XMP_Environment.h"	// ! This must be the first include.

#include "XMPFiles/source/XMPFiles_Impl.hpp"
#include "XMPFiles/source/FormatSupport/ID3_Support.hpp"
#include "XMPFiles/source/FormatSupport/Reconcile_Impl.hpp"

#include "source/UnicodeConversions.hpp"
#include "source/XIO.hpp"

#include <vector>

#define MIN(a,b)	((a) < (b) ? (a) : (b))

#if !XMP_WinBuild
	int stricmp ( const char * left, const char * right )	// Case insensitive ASCII compare.
	{
		char chL = *left;	// ! Allow for 0 passes in the loop (one string is empty).
		char chR = *right;	// ! Return -1 for stricmp ( "a", "Z" ).

		for ( ; (*left != 0) && (*right != 0); ++left, ++right ) {
			chL = *left;
			chR = *right;
			if ( chL == chR ) continue;
			if ( ('A' <= chL) && (chL <= 'Z') ) chL |= 0x20;
			if ( ('A' <= chR) && (chR <= 'Z') ) chR |= 0x20;
			if ( chL != chR ) break;
		}

		if ( chL == chR ) return 0;
		if ( chL < chR ) return -1;
		return 1;
	}
#endif

namespace ID3_Support {

// =================================================================================================

ID3GenreMap* kMapID3GenreCodeToName = 0;	// Map from a code like "21" or "RX" to the full name.
ID3GenreMap* kMapID3GenreNameToCode = 0;	// Map from the full name to a code like "21" or "RX".

static size_t numberedGenreCount = 0;	// Set in InitializeGlobals, used in ID3v1Tag::read and write.

struct GenreInfo { const char * code; const char * name; };

static const GenreInfo kAbbreviatedGenres[] = {	// ID3 v3 or v4 genre abbreviations.
	{ "RX", "Remix" },
	{ "CR", "Cover" },
	{ 0, 0 }
};

static const GenreInfo kNumberedGenres[] = {	// Numeric genre codes from ID3 v1, complete range of 0..125.
	{   "0", "Blues" },
	{   "1", "Classic Rock" },
	{   "2", "Country" },
	{   "3", "Dance" },
	{   "4", "Disco" },
	{   "5", "Funk" },
	{   "6", "Grunge" },
	{   "7", "Hip-Hop" },
	{   "8", "Jazz" },
	{   "9", "Metal" },
	{  "10", "New Age" },
	{  "11", "Oldies" },
	{  "12", "Other" },
	{  "13", "Pop" },
	{  "14", "R&B" },
	{  "15", "Rap" },
	{  "16", "Reggae" },
	{  "17", "Rock" },
	{  "18", "Techno" },
	{  "19", "Industrial" },
	{  "20", "Alternative" },
	{  "21", "Ska" },
	{  "22", "Death Metal" },
	{  "23", "Pranks" },
	{  "24", "Soundtrack" },
	{  "25", "Euro-Techno" },
	{  "26", "Ambient" },
	{  "27", "Trip-Hop" },
	{  "28", "Vocal" },
	{  "29", "Jazz+Funk" },
	{  "30", "Fusion" },
	{  "31", "Trance" },
	{  "32", "Classical" },
	{  "33", "Instrumental" },
	{  "34", "Acid" },
	{  "35", "House" },
	{  "36", "Game" },
	{  "37", "Sound Clip" },
	{  "38", "Gospel" },
	{  "39", "Noise" },
	{  "40", "AlternRock" },
	{  "41", "Bass" },
	{  "42", "Soul" },
	{  "43", "Punk" },
	{  "44", "Space" },
	{  "45", "Meditative" },
	{  "46", "Instrumental Pop" },
	{  "47", "Instrumental Rock" },
	{  "48", "Ethnic" },
	{  "49", "Gothic" },
	{  "50", "Darkwave" },
	{  "51", "Techno-Industrial" },
	{  "52", "Electronic" },
	{  "53", "Pop-Folk" },
	{  "54", "Eurodance" },
	{  "55", "Dream" },
	{  "56", "Southern Rock" },
	{  "57", "Comedy" },
	{  "58", "Cult" },
	{  "59", "Gangsta" },
	{  "60", "Top 40" },
	{  "61", "Christian Rap" },
	{  "62", "Pop/Funk" },
	{  "63", "Jungle" },
	{  "64", "Native American" },
	{  "65", "Cabaret" },
	{  "66", "New Wave" },
	{  "67", "Psychadelic" },
	{  "68", "Rave" },
	{  "69", "Showtunes" },
	{  "70", "Trailer" },
	{  "71", "Lo-Fi" },
	{  "72", "Tribal" },
	{  "73", "Acid Punk" },
	{  "74", "Acid Jazz" },
	{  "75", "Polka" },
	{  "76", "Retro" },
	{  "77", "Musical" },
	{  "78", "Rock & Roll" },
	{  "79", "Hard Rock" },
	{  "80", "Folk" },
	{  "81", "Folk-Rock" },
	{  "82", "National Folk" },
	{  "83", "Swing" },
	{  "84", "Fast Fusion" },
	{  "85", "Bebob" },
	{  "86", "Latin" },
	{  "87", "Revival" },
	{  "88", "Celtic" },
	{  "89", "Bluegrass" },
	{  "90", "Avantgarde" },
	{  "91", "Gothic Rock" },
	{  "92", "Progressive Rock" },
	{  "93", "Psychedelic Rock" },
	{  "94", "Symphonic Rock" },
	{  "95", "Slow Rock" },
	{  "96", "Big Band" },
	{  "97", "Chorus" },
	{  "98", "Easy Listening" },
	{  "99", "Acoustic" },
	{ "100", "Humour" },
	{ "101", "Speech" },
	{ "102", "Chanson" },
	{ "103", "Opera" },
	{ "104", "Chamber Music" },
	{ "105", "Sonata" },
	{ "106", "Symphony" },
	{ "107", "Booty Bass" },
	{ "108", "Primus" },
	{ "109", "Porn Groove" },
	{ "110", "Satire" },
	{ "111", "Slow Jam" },
	{ "112", "Club" },
	{ "113", "Tango" },
	{ "114", "Samba" },
	{ "115", "Folklore" },
	{ "116", "Ballad" },
	{ "117", "Power Ballad" },
	{ "118", "Rhythmic Soul" },
	{ "119", "Freestyle" },
	{ "120", "Duet" },
	{ "121", "Punk Rock" },
	{ "122", "Drum Solo" },
	{ "123", "A capella" },	// ! Should be Acapella, keep space for compatibility with old code.
	{ "124", "Euro-House" },
	{ "125", "Dance Hall" },
	{ 0, 0 }
};

// =================================================================================================

bool InitializeGlobals()
{

	kMapID3GenreCodeToName = new ID3GenreMap;
	if ( kMapID3GenreCodeToName == 0 ) return false;
	kMapID3GenreNameToCode = new ID3GenreMap;
	if ( kMapID3GenreNameToCode == 0 ) return false;
	
	ID3GenreMap::value_type newValue;
	
	size_t i;
	
	for ( i = 0; kNumberedGenres[i].code != 0; ++i ) {
		XMP_Assert ( (long)i == strtol ( kNumberedGenres[i].code, 0, 10 ) );
		ID3GenreMap::value_type code2Name ( kNumberedGenres[i].code, kNumberedGenres[i].name );
		kMapID3GenreCodeToName->insert ( kMapID3GenreCodeToName->end(), code2Name );
		ID3GenreMap::value_type name2Code ( kNumberedGenres[i].name, kNumberedGenres[i].code );
		kMapID3GenreNameToCode->insert ( kMapID3GenreNameToCode->end(), name2Code );
	}
	
	numberedGenreCount = i;	// Used in ID3v1Tag::read and write.
	
	for ( i = 0; kAbbreviatedGenres[i].code != 0; ++i ) {
		ID3GenreMap::value_type code2Name ( kAbbreviatedGenres[i].code, kAbbreviatedGenres[i].name );
		kMapID3GenreCodeToName->insert ( kMapID3GenreCodeToName->end(), code2Name );
		ID3GenreMap::value_type name2Code ( kAbbreviatedGenres[i].name, kAbbreviatedGenres[i].code );
		kMapID3GenreNameToCode->insert ( kMapID3GenreNameToCode->end(), name2Code );
	}

	return true;

}	// InitializeGlobals

// =================================================================================================

void TerminateGlobals()
{
	delete kMapID3GenreCodeToName;
	delete kMapID3GenreNameToCode;
	kMapID3GenreCodeToName = kMapID3GenreNameToCode = 0;
}

// =================================================================================================
// GenreUtils
// =================================================================================================

const char * GenreUtils::FindGenreName ( const std::string & code )
{
	// Lookup a genre code and return its name if known, otherwise 0.
	
	const char * name = 0;
	ID3GenreMap::iterator mapPos = kMapID3GenreCodeToName->find ( code.c_str() );
	if ( mapPos != kMapID3GenreCodeToName->end() ) name = mapPos->second;
	return name;

}

// =================================================================================================
	
const char * GenreUtils::FindGenreCode ( const std::string & name )
{
	// Lookup a genre name and return its code if known, otherwise 0.
	
	const char * code = 0;
	ID3GenreMap::iterator mapPos = kMapID3GenreNameToCode->find ( name.c_str() );
	if ( mapPos != kMapID3GenreNameToCode->end() ) code = mapPos->second;
	return code;

}

// =================================================================================================

static void StripOutsideSpaces ( std::string * value )
{
	size_t length = value->size();
	size_t first, last;
	
	for ( first = 0; ((first < length) && ((*value)[first] == ' ')); ++first ) {}
	if ( first == length ) { value->erase(); return; }
	XMP_Assert ( (first < length) && ((*value)[first] != ' ') );
	
	for ( last = length-1; ((last > first) && ((*value)[last] == ' ')); --last ) {}
	if ( (first == 0) && (last == length-1) ) return;
	
	size_t newLen = last - first + 1;
	if ( newLen < length ) *value = value->substr ( first, newLen );
	
}

// =================================================================================================

void GenreUtils::ConvertGenreToXMP ( const char * id3Genre, std::string * xmpGenre )
{
	// If the first character of TCON is not '(' then the entire TCON value is taken as the genre
	// name and the suffix is empty.
	//
	// If the first character of TCON is '(' then the string up to ')' (or the end) is taken as the
	// coded genre name. The rest of the TCON value after ')' is taken as the suffix.
	//
	// If the coded name is known then the corresponsing full name is used as the genre name, with
	// no parens.
	//
	// If the coded name is not known then the coded name with parens is used as the genre name.
	//
	// The value of xmpDM:genre begins with the genre name. If the suffix is not empty we append
	// "; " and the suffix. The known coded genre names currently do not use semicolon.
	//
	// Keeping the parens when importing unknown coded names might seem odd. But it preserves the
	// ID3 syntax when exporting. Otherwise we would import "(XX)" and export "XX". We don't add
	// parens all the time on export, that would import "Blues/R&B" and export "(Blues/R&B)".

	xmpGenre->erase();
	size_t id3Length = strlen ( id3Genre );
	if ( id3Length == 0 ) return;
	
	if ( id3Genre[0] != '(' ) {
		// No left paren, take the whole TCON value as the XMP value.
		xmpGenre->assign ( id3Genre, id3Length );
		StripOutsideSpaces ( xmpGenre );
		return;
	}
	
	// The first character of TCON is '(', process the coded part and the suffix.
	
	size_t codeEnd;
	std::string genreCode, suffix;

	for ( codeEnd = 1; ((codeEnd < id3Length) && (id3Genre[codeEnd] != ')')); ++codeEnd ) {}
	genreCode.assign ( &id3Genre[1], codeEnd-1 );
	if ( codeEnd < id3Length ) suffix.assign ( &id3Genre[codeEnd+1], id3Length-codeEnd-1 );

	StripOutsideSpaces ( &genreCode );
	StripOutsideSpaces ( &suffix );

	if ( genreCode.empty() ) {

		(*xmpGenre) = suffix;	// Degenerate case of "()suffix", treat as if "suffix".

	} else {

		const char * fullName = FindGenreName ( genreCode );

		if ( fullName != 0 ) {
			(*xmpGenre) = fullName;
		} else {
			(*xmpGenre) = '(';
			(*xmpGenre) += genreCode;
			(*xmpGenre) += ')';
		}

		if ( ! suffix.empty() ) {
			(*xmpGenre) += "; ";
			(*xmpGenre) += suffix;
		}

	}

}

// =================================================================================================

void GenreUtils::ConvertGenreToID3 ( const char * xmpGenre, std::string * id3Genre )
{
	// The genre name is the xmpDM:genre value up to ';', with spaces at the front or back removed.
	// The suffix is everything after ';', also with spaces at the front or back removed.
	//
	// If the genre name is known, it is replaced by the coded name in parens.
	//
	// The TCON value is the genre name plus the suffix. If the genre name does not end in ')' then
	// a space is inserted.
	
	id3Genre->erase();
	size_t xmpLength = strlen ( xmpGenre );
	if ( xmpLength == 0 ) return;
	
	size_t nameEnd;
	std::string genreName, suffix;
	
	for ( nameEnd = 0; ((nameEnd < xmpLength) && (xmpGenre[nameEnd] != ';')); ++nameEnd ) {}
	genreName.assign ( xmpGenre, nameEnd );
	if ( nameEnd < xmpLength ) suffix.assign ( &xmpGenre[nameEnd+1], xmpLength-nameEnd-1 );

	StripOutsideSpaces ( &genreName );
	StripOutsideSpaces ( &suffix );
	
	if ( genreName.empty() ) {

		(*id3Genre) = suffix;	// Degenerate case of "; suffix", treat as if "suffix".

	} else {

		const char * codedName = FindGenreCode ( genreName );
		if ( codedName != 0 ) {
			genreName = '(';
			genreName += codedName;
			genreName += ')';
		}
		
		(*id3Genre) = genreName;
		if ( ! suffix.empty() ) {
			if ( genreName[genreName.size()-1] != ')' ) (*id3Genre) += ' ';
			(*id3Genre) += suffix;
		}

	}

}

// =================================================================================================
// ID3Header
// =================================================================================================

bool ID3Header::read ( XMP_IO* file )
{

	XMP_Assert ( sizeof(fields) == kID3_TagHeaderSize );
	file->ReadAll ( this->fields, kID3_TagHeaderSize );

	if ( ! CheckBytes ( &this->fields[ID3Header::o_id], "ID3", 3 ) ) {
		// chuck in default contents:
		const static char defaultHeader[kID3_TagHeaderSize] = { 'I', 'D', '3', 3, 0, 0, 0, 0, 0, 0 };
		memcpy ( this->fields, defaultHeader, kID3_TagHeaderSize );
		return false; // no header found (o.k.) thus stick with new, default header constructed above
	}

	XMP_Uns8 major = this->fields[o_vMajor];
	XMP_Uns8 minor = this->fields[o_vMinor];
	XMP_Validate ( ((2 <= major) && (major <= 4)), "Invalid ID3 major version", kXMPErr_BadFileFormat );

	return true;

}

// =================================================================================================

void ID3Header::write ( XMP_IO* file, XMP_Int64 tagSize )
{

	XMP_Assert ( ((XMP_Int64)kID3_TagHeaderSize <= tagSize) && (tagSize < 256*1024*1024) );	// 256 MB limit due to synching.

	XMP_Uns32 synchSize = int32ToSynch ( (XMP_Uns32)tagSize - kID3_TagHeaderSize );
	PutUns32BE ( synchSize, &this->fields[ID3Header::o_size] );
	file->Write ( this->fields, kID3_TagHeaderSize );

}

// =================================================================================================
// ID3v2Frame
// =================================================================================================

#define frameDefaults	id(0), flags(0), content(0), contentSize(0), active(true), changed(false)

ID3v2Frame::ID3v2Frame() : frameDefaults
{
	XMP_Assert ( sizeof(fields) == kV23_FrameHeaderSize );	// Only need to do this in one place.
	memset ( this->fields, 0, kV23_FrameHeaderSize );
}

// =================================================================================================

ID3v2Frame::ID3v2Frame ( XMP_Uns32 id ) : frameDefaults
{
	memset ( this->fields, 0, kV23_FrameHeaderSize );
	this->id = id;
	PutUns32BE ( id, &this->fields[o_id] );
}

// =================================================================================================

void ID3v2Frame::release()
{
	if ( this->content != 0 ) delete [] this->content;
	this->content = 0;
	this->contentSize = 0;
}

// =================================================================================================

void ID3v2Frame::setFrameValue ( const std::string& rawvalue, bool needDescriptor,
											  bool utf16, bool isXMPPRIVFrame, bool needEncodingByte )
{

	std::string value;

	if ( isXMPPRIVFrame ) {

		XMP_Assert ( (! needDescriptor) && (! utf16) );

		value.append ( "XMP\0", 4 );
		value.append ( rawvalue );
		value.append ( "\0", 1  ); // final zero byte

	} else {

		if ( needEncodingByte ) {
			if ( utf16 ) {
				value.append ( "\x1", 1  );
			} else {
				value.append ( "\x0", 1  );
			}
		}

		if ( needDescriptor ) value.append ( "eng", 3 );

		if ( utf16 ) {

			if ( needDescriptor ) value.append ( "\xFF\xFE\0\0", 4 );

			value.append ( "\xFF\xFE", 2 );
			std::string utf16str;
			ToUTF16 ( (XMP_Uns8*) rawvalue.c_str(), rawvalue.size(), &utf16str, false );
			value.append ( utf16str );
			value.append ( "\0\0", 2 );

		} else {

			std::string convertedValue;
			ReconcileUtils::UTF8ToLatin1 ( rawvalue.c_str(), rawvalue.size(), &convertedValue );

			if ( needDescriptor ) value.append ( "\0", 1 );
			value.append ( convertedValue );
			value.append ( "\0", 1  );

		}

	}

	this->changed = true;
	this->release();

	this->contentSize = (XMP_Int32) value.size();
	XMP_Validate ( (this->contentSize < 20*1024*1024), "XMP Property exceeds 20MB in size", kXMPErr_InternalFailure );
	this->content = new char [ this->contentSize ];
	memcpy ( this->content, value.c_str(), this->contentSize );

}	// ID3v2Frame::setFrameValue

// =================================================================================================

XMP_Int64 ID3v2Frame::read ( XMP_IO* file, XMP_Uns8 majorVersion )
{
	XMP_Assert ( (2 <= majorVersion) && (majorVersion <= 4) );

	this->release(); // ensures/allows reuse of 'curFrame'
	XMP_Int64 start = file->Offset();
	
	if ( majorVersion > 2 ) {
		file->ReadAll ( this->fields, kV23_FrameHeaderSize );
	} else {
		// Read the 6 byte v2.2 header into the 10 byte form.
		memset ( this->fields, 0, kV23_FrameHeaderSize );	// Clear all of the bytes.
		file->ReadAll ( &this->fields[o_id], 3 );		// Leave the low order byte as zero.
		file->ReadAll ( &this->fields[o_size+1], 3 );	// Read big endian UInt24.
	}

	this->id = GetUns32BE ( &this->fields[o_id] );

	if ( this->id == 0 ) {
		file->Seek ( start, kXMP_SeekFromStart );	// Zero ID must mean nothing but padding.
		return 0;
	}

	this->flags = GetUns16BE ( &this->fields[o_flags] );
	XMP_Validate ( (0 == (this->flags & 0xEE)), "invalid lower bits in frame flags", kXMPErr_BadFileFormat );

	//*** flag handling, spec :429ff aka line 431ff  (i.e. Frame should be discarded)
	//  compression and all of that..., unsynchronisation
	this->contentSize = GetUns32BE ( &this->fields[o_size] );
	if ( majorVersion == 4 ) this->contentSize = synchToInt32 ( this->contentSize );

	XMP_Validate ( (this->contentSize >= 0), "negative frame size", kXMPErr_BadFileFormat );
	XMP_Validate ( (this->contentSize < 20*1024*1024), "single frame exceeds 20MB", kXMPErr_BadFileFormat );

	this->content = new char [ this->contentSize ];

	file->ReadAll ( this->content, this->contentSize );
	return file->Offset() - start;

}	// ID3v2Frame::read

// =================================================================================================

void ID3v2Frame::write ( XMP_IO* file, XMP_Uns8 majorVersion )
{
	XMP_Assert ( (2 <= majorVersion) && (majorVersion <= 4) );

	if ( majorVersion < 4 ) {
		PutUns32BE ( this->contentSize, &this->fields[o_size] );
	} else {
		PutUns32BE ( int32ToSynch ( this->contentSize ), &this->fields[o_size] );
	}

	if ( majorVersion > 2 ) {
		file->Write ( this->fields, kV23_FrameHeaderSize );
	} else {
		file->Write ( &this->fields[o_id], 3 );
		file->Write ( &this->fields[o_size+1], 3 );
	}

	file->Write ( this->content, this->contentSize );

}	// ID3v2Frame::write

// =================================================================================================
		
bool ID3v2Frame::advancePastCOMMDescriptor ( XMP_Int32& pos )
{

		if ( (this->contentSize - pos) <= 3 ) return false; // silent error, no room left behing language tag
		if ( ! CheckBytes ( &this->content[pos], "eng", 3 ) ) return false; // not an error, but leave all non-eng tags alone...

		pos += 3; // skip lang tag
		if ( pos >= this->contentSize ) return false; // silent error

		while ( pos < this->contentSize ) {
			if ( this->content[pos++] == 0x00 ) break;
		}
		if ( (pos < this->contentSize) && (this->content[pos] == 0x00) ) pos++;

		if ( (pos == 5) && (this->contentSize == 6) && (GetUns16BE(&this->content[4]) == 0x0031) ) {
			return false;
		}

		if ( pos > 4 ) {
			std::string descriptor = std::string ( &this->content[4], pos-1 );
			if ( 0 == descriptor.substr(0,4).compare( "iTun" ) ) {	// begins with engiTun ?
				return false; // leave alone, then
			}
		}

		return true; //o.k., descriptor skipped, time for the real thing.

}	// ID3v2Frame::advancePastCOMMDescriptor

// =================================================================================================

bool ID3v2Frame::getFrameValue ( XMP_Uns8 majorVersion, XMP_Uns32 logicalID, std::string* utf8string )
{

	XMP_Assert ( (this->content != 0) && (this->contentSize >= 0) && (this->contentSize < 20*1024*1024) );

	if ( this->contentSize == 0 ) {
		utf8string->erase();
		return true; // ...it is "of interest", even if empty contents.
	}

	XMP_Int32 pos = 0;
	XMP_Uns8 encByte = 0;
	// WCOP does not have an encoding byte, for all others: use [0] as EncByte, advance pos
	if ( logicalID != 0x57434F50 ) {
		encByte = this->content[0];
		pos++;
	}

	// mode specific forks, COMM or USLT
	bool commMode = ( (logicalID == 0x434F4D4D) || (logicalID == 0x55534C54) );

	switch ( encByte ) {

		case 0: //ISO-8859-1, 0-terminated
		{
			if ( commMode && (! advancePastCOMMDescriptor ( pos )) ) return false; // not a frame of interest!

			char* localPtr  = &this->content[pos];
			size_t localLen = this->contentSize - pos;
			ReconcileUtils::Latin1ToUTF8 ( localPtr, localLen, utf8string );
			break;

		}

		case 1: // Unicode, v2.4: UTF-16 (undetermined Endianess), with BOM, terminated 0x00 00
		case 2: // UTF-16BE without BOM, terminated 0x00 00
		{

			if ( commMode && (! advancePastCOMMDescriptor ( pos )) ) return false; // not a frame of interest!

			std::string tmp ( this->content, this->contentSize );
			bool bigEndian = true;	// assume for now (if no BOM follows)

			if ( GetUns16BE ( &this->content[pos] ) == 0xFEFF ) {
				pos += 2;
				bigEndian = true;
			} else if ( GetUns16BE ( &this->content[pos] ) == 0xFFFE ) {
				pos += 2;
				bigEndian = false;
			}

			FromUTF16 ( (UTF16Unit*)&this->content[pos], ((this->contentSize - pos)) / 2, utf8string, bigEndian );
			break;

		}

		case 3: // UTF-8 unicode, terminated \0
		{
			if ( commMode && (! advancePastCOMMDescriptor ( pos )) ) return false; // not a frame of interest!
		
			if ( (GetUns32BE ( &this->content[pos]) & 0xFFFFFF00 ) == 0xEFBBBF00 ) {
				pos += 3;	// swallow any BOM, just in case
			}

			utf8string->assign ( &this->content[pos], (this->contentSize - pos) );
			break;
		}

		default:
			XMP_Throw ( "unknown text encoding", kXMPErr_BadFileFormat ); //COULDDO assume latin-1 or utf-8 as best-effort
			break;

	}

	return true;

}	// ID3v2Frame::getFrameValue

// =================================================================================================
// ID3v1Tag
// =================================================================================================

bool ID3v1Tag::read ( XMP_IO* file, SXMPMeta* meta )
{
	// Returns true if ID3v1 (or v1.1) exists, otherwise false, sets XMP properties en route.

	if ( file->Length() <= 128 ) return false;  // ensure sufficient room
	file->Seek ( -128, kXMP_SeekFromEnd );

	XMP_Uns32 tagID = XIO::ReadInt32_BE ( file );
	tagID = tagID & 0xFFFFFF00; // wipe 4th byte
	if ( tagID != 0x54414700 ) return false; // must be "TAG"
	file->Seek ( -1, kXMP_SeekFromCurrent  ); //rewind 1

	XMP_Uns8 buffer[31]; // nothing is bigger here, than 30 bytes (offsets [0]-[29])
	buffer[30] = 0;		 // wipe last byte
	std::string utf8string;

	file->ReadAll ( buffer, 30 );
	std::string title ( (char*) buffer ); //security: guaranteed to 0-terminate after 30 bytes
	if ( ! title.empty() ) {
		ReconcileUtils::Latin1ToUTF8 ( title.c_str(), title.size(), &utf8string );
		meta->SetLocalizedText ( kXMP_NS_DC, "title", "", "x-default", utf8string.c_str() );
	}

	file->ReadAll ( buffer, 30 );
	std::string artist( (char*) buffer );
	if ( ! artist.empty() ) {
		ReconcileUtils::Latin1ToUTF8 ( artist.c_str(), artist.size(), &utf8string );
		meta->SetProperty ( kXMP_NS_DM, "artist", utf8string.c_str() );
	}

	file->ReadAll ( buffer, 30 );
	std::string album( (char*) buffer );
	if ( ! album.empty() ) {
		ReconcileUtils::Latin1ToUTF8 ( album.c_str(), album.size(), &utf8string );
		meta->SetProperty ( kXMP_NS_DM, "album", utf8string.c_str() );
	}

	file->ReadAll ( buffer, 4 );
	buffer[4]=0; // ensure 0-term
	std::string year( (char*) buffer );
	if ( ! year.empty() ) {	// should be moot for a year, but let's be safe:
		ReconcileUtils::Latin1ToUTF8 ( year.c_str(), year.size(), &utf8string );
		meta->SetProperty ( kXMP_NS_XMP, "CreateDate",  utf8string.c_str() );
	}

	file->ReadAll ( buffer, 30 );
	std::string comment( (char*) buffer );
	if ( ! comment.empty() ) {
		ReconcileUtils::Latin1ToUTF8 ( comment.c_str(), comment.size(), &utf8string );
		meta->SetProperty ( kXMP_NS_DM, "logComment", utf8string.c_str() );
	}

	if ( buffer[28] == 0 ) {
		XMP_Uns8 trackNo = buffer[29];
		if ( trackNo > 0 ) {
			std::string trackStr;
			meta->SetProperty_Int ( kXMP_NS_DM, "trackNumber", trackNo );
		}
	}

	XMP_Uns8 genreNo = XIO::ReadUns8 ( file );
	if ( genreNo < numberedGenreCount ) {
		meta->SetProperty ( kXMP_NS_DM, "genre", kNumberedGenres[genreNo].name );
	} else {
		char buffer[4];	// AUDIT: Big enough for UInt8.
		snprintf ( buffer, 4, "%d", genreNo );
		XMP_Assert ( strlen(buffer) == 3 );	// Should be in the range 126..255.
		meta->SetProperty ( kXMP_NS_DM, "genre", buffer );
	}

	return true; // ID3Tag found

}	// ID3v1Tag::read

// =================================================================================================

static inline bool GetDecimalUns32 ( const char * str, XMP_Uns32 * bin )
{
	XMP_Assert ( bin != 0 );
	if ( (str == 0) || (str[0] == 0) ) return false;
	
	*bin = 0;
	for ( size_t i = 0; str[i] != 0; ++i ) {
		char ch = str[i];
		if ( (ch < '0') || (ch > '9') ) return false;
		*bin = (*bin * 10) + (ch - '0');
	}
	
	return true;

}

// =================================================================================================

void ID3v1Tag::write ( XMP_IO* file, SXMPMeta* meta )
{

	std::string zeros ( 128, '\0' );
	std::string utf8, latin1;

	file->Seek ( -128, kXMP_SeekFromEnd );
	file->Write ( zeros.data(), 128 );

	file->Seek ( -128, kXMP_SeekFromEnd );
	XIO::WriteUns8 ( file, 'T' );
	XIO::WriteUns8 ( file, 'A' );
	XIO::WriteUns8 ( file, 'G' );

	if ( meta->GetLocalizedText ( kXMP_NS_DC, "title", "", "x-default", 0, &utf8, 0 ) ) {
		file->Seek ( (-128 + 3), kXMP_SeekFromEnd );
		ReconcileUtils::UTF8ToLatin1 ( utf8.c_str(), utf8.size(), &latin1 );
		file->Write ( latin1.c_str(), MIN ( 30, (XMP_Int32)latin1.size() ) );
	}

	if ( meta->GetProperty ( kXMP_NS_DM, "artist", &utf8, 0 ) ) {
		file->Seek ( (-128 + 33), kXMP_SeekFromEnd );
		ReconcileUtils::UTF8ToLatin1 ( utf8.c_str(), utf8.size(), &latin1 );
		file->Write ( latin1.c_str(), MIN ( 30, (XMP_Int32)latin1.size() ) );
	}

	if ( meta->GetProperty ( kXMP_NS_DM, "album", &utf8, 0 ) ) {
		file->Seek ( (-128 + 63), kXMP_SeekFromEnd );
		ReconcileUtils::UTF8ToLatin1 ( utf8.c_str(), utf8.size(), &latin1 );
		file->Write ( latin1.c_str(), MIN ( 30, (XMP_Int32)latin1.size() ) );
	}

	if ( meta->GetProperty ( kXMP_NS_XMP, "CreateDate", &utf8, 0 ) ) {
		XMP_DateTime dateTime;
		SXMPUtils::ConvertToDate( utf8, &dateTime );
		if ( dateTime.hasDate ) {
			SXMPUtils::ConvertFromInt ( dateTime.year, "", &latin1 );
			file->Seek ( (-128 + 93), kXMP_SeekFromEnd );
			file->Write ( latin1.c_str(), MIN ( 4, (XMP_Int32)latin1.size() ) );
		}
	}

	if ( meta->GetProperty ( kXMP_NS_DM, "logComment", &utf8, 0 ) ) {
		file->Seek ( (-128 + 97), kXMP_SeekFromEnd );
		ReconcileUtils::UTF8ToLatin1 ( utf8.c_str(), utf8.size(), &latin1 );
		file->Write ( latin1.c_str(), MIN ( 30, (XMP_Int32)latin1.size() ) );
	}

	if ( meta->GetProperty ( kXMP_NS_DM, "genre", &utf8, 0 ) ) {

		// Write the first genre code as a UInt8.
		size_t nameEnd;
		std::string name;
		
		for ( nameEnd = 0; ((nameEnd < utf8.size()) && (utf8[nameEnd] != ';')); ++nameEnd ) {}
		name.assign ( utf8.c_str(), nameEnd );
		const char * code = GenreUtils::FindGenreCode ( name );

		if ( code != 0 ) {
			XMP_Uns32 value;
			bool ok = GetDecimalUns32 ( code, &value );
			if ( ok && (value <= 255) ) {
				file->Seek ( (-128 + 127), kXMP_SeekFromEnd );
				XIO::WriteUns8 ( file, (XMP_Uns8)value );
			}
		}

	}

	if ( meta->GetProperty ( kXMP_NS_DM, "trackNumber", &utf8, 0 ) ) {

		XMP_Uns8 trackNo = 0;
		try {
			trackNo = (XMP_Uns8) SXMPUtils::ConvertToInt ( utf8.c_str() );
			file->Seek ( (-128 + 125), kXMP_SeekFromEnd );
			XIO::WriteUns8 ( file, 0 ); // ID3v1.1 extension
			XIO::WriteUns8 ( file, trackNo );
		} catch ( ... ) {
			// forgive, just don't set this one.
		}

	}

}	// ID3v1Tag::write

// =================================================================================================

};	// namespace ID3_Support