/*
 *  Region X:
 *
 *  When hardware (that is, the DVD drive) is region free, the CSS license requires that the region lock be software enforced.
 *  On MacOS X, this is done in a way that depends on the OS version:
 *  - Earlier OS X version stored the region settings in a user preference file, in the SSPD property.
 *  - Starting from DVD Player 3.0.1, the settings are also stored as Open Firmware/EFI variables. Modifying them
 *    requires root privilege.
 *  Region X will read, edit and write or delete these settings.
 *
 *	To access the settings from a terminal, type:
 *	nvram Region-Changes Region-Code; defaults read com.apple.DVDPlayer SSPD
 *  That's all about MacOS region settings for RPC-1 drives. The rest is common MacOS X programming.
 *
 * Version notes:
 *	1.2: * Universal binary, so requires OS X 10.3.9 and up. (later, I tested that Universal Binaries *do* run on 10.3.0)
 *  1.3: * The SSPD was skipped (i.e. neither read nor written). Note to myself: This was very baaaad.
 *  1.4: * The SSPD handling was re-added and corrected. Shame on me: the SSPD is endian sensitive except for
 *          the FORM, DVDC and DVDR items.
 *       * The bundle name is now com.apple.DVDPlayer (requires DVD Player 4.0 and up), the old com.apple.dvdplayer
 *          has been dropped as OS X 10.3.9 and up (as required by the universal binary) do not use it.
 *  1.5: * DVD Player and Front Row are now quitted and optionally launched.
 *       * Front Row behaves the following:
 *         - it is launched by calling BSRemoteUIToggle() from the private BezelServices framework, as launching the
 *           com.apple.frontrowlauncher application doesn't work if it's not present (Think: Using the Apple Remote)
 *         - it is terminated with a SIGTERM signal, searching the com.apple.frontrow application using the Carbon
 *           Process Manager API as it is not member of the NSWorkSpace launchedApplications array.
 *       * OF/EFI variables and SSPD preference key are now deleted when no region is set, if possible. This is good way to clean up.
 *       * OF/EFI variables are now written/deleted using Apple's nvram code base, much better than mine.
 *       * Compiled with XCode 3.1
 *  1.6: * Added a new PPC only version for older OS X versions.
 *       * Re-added the dual DVD Player bundle name handling, for older OS X versions.
 *       * Corrected: Changes left could be modified when Region X was launched.
 *       * Compiled with XCode 3.1.1
 *       * Tested on OS X as low as 10.3.0
 *  1.7: * Default region when exiting the 'no region set' enhanced: the last region the user set is remembered
 *       * Compiled with XCode 3.2
 *       * Use a separate tool for privileged code to handle new 10.6 handling of setuid binaries and AppKit.
 *  1.8: * Allow a write-protected copy of Region X to work by authenticating every time.
 *  1.9: * Correction for Region X Tool with setuid bit set with owner not being root (e.g. after Finder copy).
 *       * Compiled with XCode 3.2.2
 *  2.0: * Added RPC-1 drive present check and warning if none is found. Corrected SSPD to use UInt32 instead of long
 *  2.1: * Unmounted drive on which ObtainExclusiveAccess fails are now assumed RPC-1
 *       * Corrected the RPC-1 drive scanning code
 *
 */

// Note: For CONFIGURATION_xxxxx macros to be properly defined, CONFIGURATION_$(CONFIGURATION) must be added in:
// Project/Edit Project Settings/GCC x.y Preprocessing/Preprocessor Macros Not Used In Precompiled Headers/Build/All configurations
// This requires the configuration name to be a valid identifier.

#ifdef CONFIGURATION_Debug
#endif

#import "Region X.h"
#import "Region X Tool.h"

// OS X imports
#import <Security/Security.h>							// for authorization services
#import <IOKit/IOCFBundle.h>							// for kIOBundleVersionKey
#import <CoreServices/CoreServices.h>					// for kCFBundleIdentifierKey
#import <ApplicationServices/ApplicationServices.h>		// for the Process Manager, Launch Services etc.
#import <Carbon/Carbon.h>								// for AESend

// Standard imports
#import <unistd.h>										// for fork()/execv()
#import <signal.h>										// for kill()

// My prototypes
UInt32 rotl(UInt32 x, UInt32 n);
UInt32 rotr(UInt32 x, UInt32 n);
UInt32 Parse(const UInt8 *buffer, CFIndex length);
CFIndex Print(char *buffer, UInt32 x);
void WriteNVRAM(UInt32 RegionChanges, UInt32 Code, Boolean Remove);
void ReadSettings(void);
void CheckForRepair(void);
void QuitApps(void);
int NoRPC1Drive(void);

// Private Apple Framework prototypes
extern void BSRemoteUIToggle(void) __attribute__((weak_import)); // for BezelServices (Front Row) that should be weak-linked

// Region X Tool

// plist file name for storing the SSPD property.
// DVD Player < 4.0 used com.apple.dvdplayer. DVD Player 4.0 and up (MacOS 10.3 and up) uses com.apple.DVDPlayer.
// On case sensitive filesystems such as UFS or case-sensitive HFS+, these are different file names!
#define DVDPLAYERBUNDLEID			CFSTR("com.apple.DVDPlayer")
#define DVDPLAYEROLDBUNDLEID		CFSTR("com.apple.dvdplayer")
// Front Row "background" application, a.k.a. Back Row
#define FRONTROWBUNDLEID			CFSTR("com.apple.frontrow")
// Front Row launcher, may be not present on certain configuration (Front Row is then launched via the remote controller)
#define FRONTROWLAUNCHERBUNDLEID	CFSTR("com.apple.frontrowlauncher")
// Bundle name used to remember "no application"
#define NOAPPLICATIONBUNDLE			CFSTR("")

// DVD Player preference property
#define DVDPLAYERSSPD				CFSTR("SSPD")

// DVD Player preference ASCII constants with endian correction
#define cFORM						CFSwapInt32BigToHost(0x464F524D)
#define cDVDC						CFSwapInt32BigToHost(0x44564443)
#define cDVDR						CFSwapInt32BigToHost(0x44564452)

// Region X preference properties
#define PREFAPPLICATION				CFSTR("ApplicationBundleToLaunch")
#define PREFRPC1WARNING				CFSTR("DidWarnAboutRPC1")
#define PREFLASTREGIONSET			CFSTR("LastRegionSet")

#define REGIONNOTSET				0
#define DEFAULTCHANGES				5
#define DEFAULTREGION				REGIONNOTSET

#define ROOTUSER					@"root"
#define ROOTGROUP					@"wheel"

#define BIT32(n)					(((UInt32) 1) << n)

typedef struct						// Format of the structure that stores the region settings in DVD Player user's preference file.
{
	UInt32 FORM;
	UInt32 x14;
	UInt32 DVDC;
	UInt32 DVDR;
	UInt32 x0c;
	UInt32 SSPDRegion;
	UInt32 SSPDChanges;
	UInt32 x05;
} SSPD;

// Just a few global variables, names start with 'z'.
static unsigned long zRegion = DEFAULTREGION;			// 0 if not set, 1 to 8 otherwise
static unsigned long zRegionWhenSet;					// As zRegion, except it will never be REGIONNOTSET, defaults to last region set by user
static unsigned long zChangesLeft = DEFAULTCHANGES;		// how much region changes are left
static CFStringRef zAppBundle;							// application bundle (or an empty string) to launch on save and quit
static CFStringRef zDVDPlayerBundleName = NULL;			// bundle name of DVD Player
static UInt32 zOSVersion;								// OS X Version (not the z/OS version, haha)
static AuthorizationRef zAuthorization = NULL;			// Authorization to call ancillary tool with root privilege

// Region[1-8] magic constants, which will be rotated by the number of changes done.
// "Region not set" is remembered by removing OF/EFI variables and SSPD preference key, or by using the 0 constant.
static const UInt32 zRegionConstant[8] = 
{
	0x43241234, 0x24534251, 0x23423142, 0x35235353,
	0x43271234, 0x24574251, 0x23473142, 0x35275353
};



UInt32 ProcessCDB(SCSITaskInterface **task, UInt8 *aCDB,UInt32 cdbLength, int direction, void *buffer, UInt32 bufferLength, UInt32 timeout);


UInt32 ProcessCDB(SCSITaskInterface **task, UInt8 *aCDB,UInt32 cdbLength, int direction, void *buffer, UInt32 bufferLength, UInt32 timeout)
{
	IOVirtualRange range;
	SCSI_Sense_Data sense;
	SCSITaskStatus taskstatus;
	IOReturn ior;
	int DataTransferDirection;
	UInt64 count;
	UInt32 actualLength;
	
	switch(direction)
	{
		case DirIn:  DataTransferDirection = kSCSIDataTransfer_FromTargetToInitiator; break;
		case DirOut: DataTransferDirection = kSCSIDataTransfer_FromInitiatorToTarget; break;
		case DirNone:
		default: DataTransferDirection = kSCSIDataTransfer_NoDataTransfer; bufferLength = 0; break;
	}
	
	range.address = (IOVirtualAddress) buffer;
	range.length = bufferLength;
	
	ior = (*task)->SetCommandDescriptorBlock(task, aCDB, cdbLength);
	if (ior != kIOReturnSuccess) return 0;
	
	ior = (*task)->SetScatterGatherEntries(task, &range, 1, bufferLength, DataTransferDirection);
	if (ior != kIOReturnSuccess) return 0;
	
	ior = (*task)->SetTimeoutDuration(task, timeout ? timeout : 10000);
	if (ior != kIOReturnSuccess) return 0;
	
	ior = (*task)->ExecuteTaskSync(task, &sense, &taskstatus, &count);
	actualLength = count & 0xFFFFFFFF;
	if ((ior == kIOReturnSuccess) && (taskstatus == kSCSITaskStatus_CHECK_CONDITION)) return ((sense.SENSE_KEY & 0x0F) << 16) | (sense.ADDITIONAL_SENSE_CODE << 8) | sense.ADDITIONAL_SENSE_CODE_QUALIFIER;
	if (ior != kIOReturnSuccess) return 0xFFFFFFFF;
	
	return actualLength;
}

@implementation Device


+ (UInt32) ScanAll
{
	io_iterator_t iterator;
	io_object_t object;
	io_object_t parentObject;
	io_object_t childObject;
	IOReturn ior;
	CFMutableDictionaryRef dictionary;
	CFMutableDictionaryRef subdictionary;
	IOCFPlugInInterface **plugininterface;
	MMCDeviceInterface **mmcinterface;
	SCSITaskDeviceInterface **scsitaskinterface;
	SCSITaskInterface **task;
	SInt32 score;
	NSString * diskName;
	char name[256];
	int ReturnCode;
	dk_dvd_report_key_t DVD;
	DVDRegionPlaybackControlInfo DVDInfo;
	int deviceFile;
	UInt8 cdb[12];
	UInt32 Drives = 0;
	UInt32 RPC1 = 0;
	UInt32 RPC2 = 0;
	NSString * vendorName;
	NSString * modelName;
	NSString * firmwareName ;
	
	// DVD devices
	dictionary = CFDictionaryCreateMutable(kCFAllocatorDefault, (CFIndex) 0, NULL, NULL);
	if (dictionary != NULL)
	{
		subdictionary = CFDictionaryCreateMutable(kCFAllocatorDefault, (CFIndex) 0, NULL, NULL);
		if (subdictionary != NULL)
		{
			// Loop over all DVD block storage devices
			CFDictionarySetValue(subdictionary, CFSTR(kIOClassKey), CFSTR(kIODVDBlockStorageDriverClass) );
			CFDictionarySetValue(dictionary, CFSTR(kIOPropertyMatchKey), subdictionary);
			ior = IOServiceGetMatchingServices(kIOMasterPortDefault, dictionary, &iterator);
			
			if (iterator != 0)
			{
				while ( (object = IOIteratorNext(iterator)) )
				{
					Drives++;
					// Get the BSD "disk.." name if the DVD drive has a mounted medium
					diskName = (NSString *) IORegistryEntrySearchCFProperty(object, kIOServicePlane, CFSTR(kIOBSDNameKey), kCFAllocatorDefault, kIORegistryIterateRecursively);
					if (diskName)
					{
						// BSD name acquired, device is mounted. Get the RPC status via ioctl()
						sprintf(name,"/dev/r%s",[diskName UTF8String]);
						deviceFile = open(name,O_RDONLY);
						if (deviceFile != -1)
						{
							memset(&DVD, 0, sizeof(DVD));
							memset(&DVDInfo, 0, sizeof(DVDInfo));
							DVD.format = kDVDKeyFormatRegionState;
							DVD.buffer = &DVDInfo;
							DVD.bufferLength = sizeof(DVDInfo);
							DVD.keyClass = kDVDKeyClassCSS_CPPM_CPRM;
							
							ReturnCode = ioctl(deviceFile, DKIOCDVDREPORTKEY, &DVD);
							if ( (ReturnCode != -1) && (DVDInfo.rpcScheme == 0) ) RPC1++;
							if ( (ReturnCode != -1) && (DVDInfo.rpcScheme != 0) ) RPC2++;
							
							// Now, scan parents for vendor/model/firmware name. Adequate parent should be "IOSCSIPeripheralDeviceNub"
							//  but scan all parents upwards until we get what we are loking for
							vendorName = nil;
							modelName = nil;
							firmwareName = nil;
							while (IORegistryEntryGetParentEntry(object, kIOServicePlane, &parentObject) == kIOReturnSuccess)
							{
								IOObjectRelease(object);
								object = parentObject;
								if (!vendorName) vendorName = (NSString *) IORegistryEntrySearchCFProperty(object, kIOServicePlane, CFSTR(kIOPropertyVendorIdentificationKey), kCFAllocatorDefault, kIORegistryIterateRecursively);
								if (!modelName) modelName = (NSString *) IORegistryEntrySearchCFProperty(object, kIOServicePlane, CFSTR(kIOPropertyProductIdentificationKey), kCFAllocatorDefault, kIORegistryIterateRecursively);
								if (!firmwareName) firmwareName = (NSString *) IORegistryEntrySearchCFProperty(object, kIOServicePlane, CFSTR(kIOPropertyProductRevisionLevelKey), kCFAllocatorDefault, kIORegistryIterateRecursively);
								if (vendorName && modelName && firmwareName) break;
							}
							IOObjectRelease(object);
						}
					}
					else
					{
						// diskName not retrieved, device probably not mounted
						// Now, scan parents for vendor/model/firmware name. Adequate parent should be "IOSCSIPeripheralDeviceNub"
						//  but scan all parents upwards until we get what we are loking for
						vendorName = nil;
						modelName = nil;
						firmwareName = nil;
						while (IORegistryEntryGetParentEntry(object, kIOServicePlane, &parentObject) == kIOReturnSuccess)
						{
							IOObjectRelease(object);
							object = parentObject;
							if (!vendorName) vendorName = (NSString *) IORegistryEntrySearchCFProperty(object, kIOServicePlane, CFSTR(kIOPropertyVendorIdentificationKey), kCFAllocatorDefault, kIORegistryIterateRecursively);
							if (!modelName) modelName = (NSString *) IORegistryEntrySearchCFProperty(object, kIOServicePlane, CFSTR(kIOPropertyProductIdentificationKey), kCFAllocatorDefault, kIORegistryIterateRecursively);
							if (!firmwareName) firmwareName = (NSString *) IORegistryEntrySearchCFProperty(object, kIOServicePlane, CFSTR(kIOPropertyProductRevisionLevelKey), kCFAllocatorDefault, kIORegistryIterateRecursively);
							if (vendorName && modelName && firmwareName) break;
						}
						
						// Now scan childs downwards until we can get the MMC device
						while (IORegistryEntryGetChildEntry(object, kIOServicePlane, &childObject) == kIOReturnSuccess)
						{
							IOObjectRelease(object);
							object = childObject;
							ior = IOCreatePlugInInterfaceForService(object, kIOMMCDeviceUserClientTypeID, kIOCFPlugInInterfaceID, &plugininterface ,&score);
							if (ior == kIOReturnSuccess)
							{
								ior = (*plugininterface)->QueryInterface(plugininterface, CFUUIDGetUUIDBytes(kIOMMCDeviceInterfaceID), (LPVOID) &mmcinterface);
								if (ior == kIOReturnSuccess)
								{
									scsitaskinterface = (*mmcinterface)->GetSCSITaskDeviceInterface(mmcinterface);
									if (scsitaskinterface != NULL)
									{
										ior = (*scsitaskinterface)->ObtainExclusiveAccess(scsitaskinterface);
										if (ior == kIOReturnSuccess)
										{
											task = (*scsitaskinterface)->CreateSCSITask(scsitaskinterface);
											if (task)
											{
												cdb[0] = 0xa4;
												cdb[1] = 0x00;
												cdb[2] = 0x00;
												cdb[3] = 0x00;
												cdb[4] = 0x00;
												cdb[5] = 0x00;
												cdb[6] = 0x00;
												cdb[7] = 0x00;
												cdb[8] = (sizeof(DVDInfo) >> 8) & 0xff;
												cdb[9] = sizeof(DVDInfo) & 0xff;
												cdb[10] = 0x08;
												cdb[11] = 0x00;
												memset(&DVDInfo, 0, sizeof(DVDInfo));
												ProcessCDB(task, cdb, 12, DirIn, &DVDInfo, sizeof(DVDInfo), 30000);
												
												if (DVDInfo.rpcScheme == 0) RPC1++;
												if (DVDInfo.rpcScheme != 0) RPC2++;
												
												(*task)->Release(task);
											}
										}
									}
									(*scsitaskinterface)->Release(scsitaskinterface);
								}
								(*plugininterface)->Release(plugininterface);
								break; // Stop looping over childs
							}
						}
						IOObjectRelease(object);
					}
				}
				IOObjectRelease(iterator);
			}
			CFRelease(subdictionary);
		}
	}
	
	if (!RPC1 && (Drives != (RPC1+RPC2)))
	{
		// No RPC1 found and some drive in a unknown RPC state, warn user
	int button;
			
		button = NSRunAlertPanel(NSLocalizedString(@"UnknownRPCState",@""), 
								 NSLocalizedString(@"RequiresRPC1Drive",@""), NSLocalizedString(@"NotRPC1",@""), NSLocalizedString(@"IsRPC1",@""), NULL);
		if (button == NSAlertDefaultReturn)
		{
			[NSApp terminate:nil];
		}
	}
	return Drives - RPC2;
}



@end



int NoRPC1Drive(void)
{
	return ([Device ScanAll] == 0);
}



// function to rotate left a 32 bit number
UInt32 rotl(UInt32 x, UInt32 n)
{
	UInt32 i;
	
	n %= 32;
	for (i = 0; i < n; i++)
	{
		if (x & BIT32(31))
			x = (x << 1) | BIT32(0);
		else
			x = (x << 1);
	}
	return x;
}



// function to rotate right a 32 bit number
UInt32 rotr(UInt32 x, UInt32 n)
{
	UInt32 i;
	
	n %= 32;
	for (i = 0; i < n; i++)
	{
		if (x & BIT32(0))
			x = (x >> 1) | BIT32(31);
		else
			x = (x >> 1);
	}
	return x;
}



// parse a decimal number from a sized buffer
// sscanf is NOT used because it requires a 0 terminated C string
UInt32 Parse(const UInt8 *buffer, CFIndex length)
{
	CFIndex i;
	UInt32 x;
	int sign = +1;
	
	x = 0;
	i = 0;
	if (i < length)
	{
		switch (buffer[i])
		{
			case '-':   // Region settings are written as signed decimal numbers
				sign = -1;
				i++;
				break;
			case '+':   // not currently used, but who knows?
				sign = +1;
				i++;
				break;
			default:
				break;
		}
	}
	
	for (; i < length; i++)
	{
		x = x*10 + buffer[i] - '0';
	}
	
	// don't compute sign*x as x is unsigned.
	if (sign < 0) return -x;
	else return x;
}



// The opposite of Parse()
// This function avoids using printf()
CFIndex Print(char *buffer, UInt32 x)
{
	CFIndex i;
	UInt32 r;
	int b;
	
	i = 0;
	
	if (x & 0x80000000) // 1.1.2: corrected from 0x8000000 to 0x80000000. A '0' was missing. Ouch.
	{
		buffer[i++] = '-';
		x = -x;
	}
	
	b = 0;
	for (r = 1000000000LU; r > 0; r /= 10) // 1.3: changed to 1000000000LU instead of 1000000000U
	{
		if ((x >= r) || (r == 1)) b = 1;
		if (b) buffer[i++] = '0' + ((x / r) % 10);
		x %= r;
	}
	
	buffer[i] = 0;	// mark end of string
	return i;
}



void WriteNVRAM(UInt32 RegionChanges, UInt32 Code, Boolean Remove)
{
	char tmpCode[32];
	char tmpChanges[32];
	NSString *executable = [[NSBundle mainBundle] pathForAuxiliaryExecutable:(NSString *)REGION_X_TOOL_NAME];
	NSFileManager *manager = [NSFileManager defaultManager];
	const char *path = [manager fileSystemRepresentationWithPath:executable];
	char *args[5]; // Maximum 5 args includng final NULL
	OSStatus err;
	int i = 0;
	
	Print(tmpCode, Code);
	Print(tmpChanges, RegionChanges);
	
	// 1.8: Region X Tool will not have its setuid bit set if on a readonly filesystem, so force calling it as root
	//      if user did authenticate. This will make Region X usable from readonly filesystems by requesting
	//      authentication everytime it is run.
	if (zAuthorization != NULL)
	{
		
		args[i++] = tmpCode;
		args[i++] = tmpChanges;
		if (Remove) args[i++] = REGION_X_TOOL_DELETE;
		args[i++] = NULL;
		
		err = AuthorizationExecuteWithPrivileges(zAuthorization, path, kAuthorizationFlagDefaults, args, NULL);
	}
	else // call the setuid ancillary tool
	{
		if (fork() == 0)
		{
			args[i++] = (char *) path;
			args[i++] = tmpCode;
			args[i++] = tmpChanges;
			if (Remove) args[i++] = REGION_X_TOOL_DELETE;
			args[i++] = NULL;
			
			execv(path,args);
		}
	}
}



// Read the settings from OF/EFI and/or user preferences. This does not require root access.
// Note that if the .plist and the OF/EFI firmware firmwares disagree, the DVD Player application trusts only one,
//   but which one may be version dependent. I trust OF/EFI as it is cross-user.
void ReadSettings(void)
{
	int i, GotFromOF, ReadCount;
	UInt32 Code = DEFAULTREGION, RegionChanges = DEFAULTCHANGES;
	UInt32 DerotatedRegionCode;
	CFDataRef theData;
	SSPD theSSPD;
	
	// First, try to read the OpenFirmware variables (for DVD Player >= 3.0.1)
	GotFromOF = 0;
	ReadCount = 0;
	
	theData = GetOFVariable(OFREGIONCHANGE);
	if (theData)
	{
		RegionChanges = Parse(CFDataGetBytePtr(theData), CFDataGetLength(theData));
		ReadCount++;
		CFRelease(theData);
	}
	
	theData = GetOFVariable(OFREGIONCODE);
	if (theData)
	{
		Code = Parse(CFDataGetBytePtr(theData), CFDataGetLength(theData));
		ReadCount++;
		CFRelease(theData);
	}
	
	if (ReadCount == 2)
	{									// if we could read both value from the NVRAM
		GotFromOF = 1;						// remember we read from OF/EFI
		zChangesLeft = 5 - RegionChanges;   // convert OF/EFI variables into our global variables
		DerotatedRegionCode = rotl(Code, RegionChanges);
		zRegion = REGIONNOTSET;
		for (i = 0; i < 8; i++)				// Find stored region from its rotated magic constant.
			if (DerotatedRegionCode == zRegionConstant[i]) zRegion = i + 1;
	}
	else
	{									// OF/EFI variables not read, use default values
		zChangesLeft = DEFAULTCHANGES;
		zRegion = DEFAULTREGION;
		RegionChanges = 5 - zChangesLeft;
		Code = zRegion;
	}
	
	// Now, read the SSPD structure from DVD Player preferences.
	// 1.6: Dual case differing bundle name handling re-added.
	zDVDPlayerBundleName = DVDPLAYERBUNDLEID;
	theData = CFPreferencesCopyAppValue(DVDPLAYERSSPD, zDVDPlayerBundleName);
	if (!theData)
	{
		// Search in the alternate bundle name. OS X <= 10.2.x used lowercase, >=10.3 use mixed case
		// This is NOT redundant, as some users have OS X case sensitive filesystems.
		// Even HFS is now case-sensitive able, so we can handle all known OS X versions on case sensitive filesystems.
		zDVDPlayerBundleName = DVDPLAYEROLDBUNDLEID;
		theData = CFPreferencesCopyAppValue(DVDPLAYERSSPD, zDVDPlayerBundleName);
		if (!theData)
		{
			if (zOSVersion < 100300)
				zDVDPlayerBundleName = DVDPLAYEROLDBUNDLEID;
			else
				zDVDPlayerBundleName = DVDPLAYERBUNDLEID;
		}
	}
	
	if ( (theData) && (CFDataGetLength(theData) == sizeof(theSSPD)) )
	{
		// The SSPD was read and has the right size.
		CFRange theRange = {0, sizeof(theSSPD)};
		
		CFDataGetBytes(theData, theRange, (UInt8 *) &theSSPD);
		
		if (!GotFromOF)							// This gives priority to OF/EFI over preference file, but not on DEFAULTREGION etc
		{									// So, parse the SSPD only if the OF/EFI variables were not read.
			if (CFSwapInt32(theSSPD.x14) == 0x00000014)		// Oooh, this means the SSPD endianness is wrong!
			{		// 1.6: Correct the endianness of the SSPD (some versions of Region X mixed it up)
				theSSPD.x14 = CFSwapInt32(theSSPD.x14);
				theSSPD.x0c = CFSwapInt32(theSSPD.x0c);
				theSSPD.x05 = CFSwapInt32(theSSPD.x05);
				theSSPD.SSPDChanges = CFSwapInt32(theSSPD.SSPDChanges);
				theSSPD.SSPDRegion = CFSwapInt32(theSSPD.SSPDRegion);
			}
			if (								// Check that SSPD is in a valid known format
				(theSSPD.FORM == cFORM) &&		// endian correction included
				(theSSPD.x14  == 0x00000014) &&	// native endianness!
				(theSSPD.DVDC == cDVDC) &&		// endian correction included
				(theSSPD.DVDR == cDVDR) &&		// endian correction included
				(theSSPD.x0c  == 0x0000000c) &&	// native endianness!
				(theSSPD.SSPDChanges <= 5) &&	// native endianness!
				(theSSPD.x05  == 0x00000005)	// native endianness!
				)
			{
				zChangesLeft = 5 - theSSPD.SSPDChanges; // convert SSPD values into our global variables
				DerotatedRegionCode = rotl(theSSPD.SSPDRegion, theSSPD.SSPDChanges);
				zRegion = REGIONNOTSET;
				for (i = 0; i < 8; i++)					// Find stored region from its rotated magic constant.
					if (DerotatedRegionCode == zRegionConstant[i]) zRegion = i + 1;
			}
			else
			{
				// Settings are corrupted or in an unknown newer format. Tell it and use default values!
				NSRunAlertPanel(NSLocalizedString(@"UnknownSettings",@""), 
								NSLocalizedString(@"SettingsCorrupted",@""), NULL, NULL, NULL);
				//			[NSApp terminate:nil]; // Doh! Removed as of version 1.2
				zChangesLeft = 5; // default values
				zRegion = REGIONNOTSET;
			}
		}
	}
	if (theData) CFRelease(theData);
}



// This function ensures Region X Tool
// It asks user for authorization and, if possible, changes the tool's executable so that it will be run with euid root next time 
void CheckForRepair(void)
{
	NSFileManager *manager = [NSFileManager defaultManager];
	NSString *executable = [[NSBundle mainBundle] pathForAuxiliaryExecutable:(NSString *)REGION_X_TOOL_NAME];
	const char *path = [manager fileSystemRepresentationWithPath:executable];
	NSDictionary *attr;
	OSStatus err;
	char *args[2] = {REGION_X_TOOL_REPAIR, NULL};
	
	if (executable)
	{   // if we somehow fail to update file's attribute, don't care as on next launch, we'll ask for authorization again.
#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
		attr = [manager attributesOfItemAtPath:executable error:NULL];
#else
		attr = [manager fileAttributesAtPath:executable traverseLink:YES];
#endif // MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_5
		
		if (attr)
		{
			if ( (![[attr fileOwnerAccountName] isEqual:ROOTUSER]) ||  
				(![[attr fileGroupOwnerAccountName] isEqual:ROOTGROUP]) ||  
				(([attr filePosixPermissions] & (S_ISUID|S_ISGID|S_IRWXU|S_IRWXG|S_IRWXO)) != REGION_X_TOOL_PERM)
				)
			{
				
				err = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagInteractionAllowed, &zAuthorization);
				if (err == errAuthorizationSuccess)
				{
					int pid, status;
					char *chmodargs[3] = {"555", NULL, NULL};
					// Finder copy (and maybe other operations) can lead to setuid bit set with non-root owner. Clean it up.
					chmodargs[1] = (char *) path;
					err = AuthorizationExecuteWithPrivileges(zAuthorization, "/bin/chmod", kAuthorizationFlagDefaults, chmodargs, NULL);
					pid = wait(&status);
					err = AuthorizationExecuteWithPrivileges(zAuthorization, path, kAuthorizationFlagDefaults, args, NULL);
					pid = wait(&status);
				}
				else
					zAuthorization = NULL;
			}
		}
	}
	
	// Switch to front
	[NSApp activateIgnoringOtherApps:YES];
}


void QuitApps(void)
{
	ProcessSerialNumber psn;
	NSDictionary *ApplInfo;
	NSString *ApplicationBundle;
	AEDesc addressDesc;
	AppleEvent event,reply;
	OSErr err;
	pid_t pid;
	
	psn.highLongOfPSN = 0;
	psn.lowLongOfPSN = kNoProcess;
	while (GetNextProcess(&psn) == noErr)
	{
		ApplInfo = (NSDictionary *) ProcessInformationCopyDictionary(&psn, kProcessDictionaryIncludeAllInformationMask);
		ApplicationBundle = [ApplInfo objectForKey:(NSString *)kCFBundleIdentifierKey];
		
		if ([ApplicationBundle isEqual:(NSString *) zDVDPlayerBundleName])
		{
			// DVD Player is best quitted with an Apple Event
			pid = [[ApplInfo objectForKey:@"pid"] intValue];
			err = AECreateDesc(typeKernelProcessID, &pid, sizeof(pid_t), &addressDesc);
			if (err == noErr) err = AECreateAppleEvent(kCoreEventClass, kAEQuitApplication, &addressDesc, kAutoGenerateReturnID, kAnyTransactionID, &event);
			if (err == noErr) err = AESend(&event, &reply, kAENoReply, kAENormalPriority, kAEDefaultTimeout, NULL, NULL);
		}
		
		if ([ApplicationBundle isEqual:(NSString *)FRONTROWBUNDLEID])
		{
			// Front Row engine (a.k.a. Back Row) is quitted by a TERM signal as it does not respond to the kAEQuitApplication event.
			// Not the nicest way to quit Front Row, but I found no better way.
			pid = [[ApplInfo objectForKey:@"pid"] intValue];
			kill(pid, SIGTERM);
		}
	}
}



@implementation RegionController


// This is called when user changes the application to launch
- (IBAction)LaunchOption:(id)sender
{
	CFIndex MenuIndex;
	MenuIndex = [ApplicationToLaunch indexOfSelectedItem];
	switch(MenuIndex)
	{
		case 1:
			zAppBundle = zDVDPlayerBundleName;
			break;
		case 2:
			zAppBundle = FRONTROWLAUNCHERBUNDLEID;
			break;
		default:
			zAppBundle = NOAPPLICATIONBUNDLE;
			break;
	}
	CFPreferencesSetAppValue(PREFAPPLICATION, zAppBundle, kCFPreferencesCurrentApplication);
	CFPreferencesAppSynchronize(kCFPreferencesCurrentApplication);
}



// This is called when user changes the "Left Changes" value
- (IBAction)ChangesLeftChanged:(id)sender
{
	zRegion = [[Region selectedCell] tag];
	zChangesLeft = [[ChangesLeft selectedCell] tag];
	
	// enforce coherent "region not set <=> 5 changes left" behavior
	if (zChangesLeft == 5)
	{
		// 5 changes left implies "Region not set".
		// remember the current region, if set, used as default region when quitting "5 changes left" mode
		if (zRegion != REGIONNOTSET) zRegionWhenSet = zRegion;
		[Region selectCellWithTag:0];
	}
	else
	{
		if (zRegion == REGIONNOTSET)
		{
			[Region selectCellWithTag:zRegionWhenSet]; // 4 and less changes left prohibits "Region not set"
		}
	}
}



// This is called when user changes the "Region"
- (IBAction)RegionChanged:(id)sender
{
	zRegion = [[Region selectedCell] tag];
	zChangesLeft = [[ChangesLeft selectedCell] tag];
	
	// enforce coherent "region not set <=> 5 changes left" behavior
	// 1.5: changing the region automatically selects 4 changes left to minimize need to run Region X again.
	// if you want to, you can modify the "changes left" setting AFTER setting the region.
	if (zRegion == REGIONNOTSET)
		[ChangesLeft selectCellWithTag:5];  // "Region not set" implies "5 changes left"
	else
		[ChangesLeft selectCellWithTag:4];
}


// This function is called at early application startup. Its purpose is to setup everything as needed.
- (void)awakeFromNib
{
	NSString *Version,*Title;
	CFStringRef DidWarnAboutRPC1;
	OSStatus theErr;
	SInt32 OSVersion, OSMajor, OSMinor, OSBugFix;
	CFURLRef AppURL;
	CFStringRef AppName;
	CFIndex MenuIndex;
	CFIndex savedRegion;
	Boolean IsValid;
	
	// 1.3: Get the current OS version.
	theErr = Gestalt(gestaltSystemVersionMajor, &OSMajor);
	if (theErr == noErr) theErr = Gestalt(gestaltSystemVersionMinor, &OSMinor);
	if (theErr == noErr) theErr = Gestalt(gestaltSystemVersionBugFix, &OSBugFix);
	if (theErr == noErr)
	{
		zOSVersion = OSMajor*10000 + OSMinor*100 + OSBugFix;
	}
	else
	{
		// Some OS X versions (e.g. 10.3) have no gestaltSystemVersionMajor/Minor/BugFix, fallback to gestaltSystemVersion
		theErr = Gestalt(gestaltSystemVersion, &OSVersion);
		if (theErr == noErr)
		{
			OSMajor  = ((OSVersion>>12) * 10) + ((OSVersion>>8) & 0xf); // convert e.g. 0x10.. to 10
			OSMinor  = (OSVersion>>4) & 0xf;
			OSBugFix = OSVersion & 0xf;
			zOSVersion = OSMajor*10000 + OSMinor*100 + OSBugFix;
		}
		else
		{
			zOSVersion = 0;
		}
	}
	
	// Get current version
	Version = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kIOBundleVersionKey];
	
	// 1.1.1: Issue a warning about RPC-1 drives at each version change, many people ignore the region free drive requirement!
	DidWarnAboutRPC1 = CFPreferencesCopyAppValue(PREFRPC1WARNING, kCFPreferencesCurrentApplication);
	if ( !DidWarnAboutRPC1 || !Version || !CFEqual(DidWarnAboutRPC1, Version) )
	{
		int button;
		
		button = NSRunAlertPanel(NSLocalizedString(@"CheckRPC1Drive",@""), 
								 NSLocalizedString(@"RequiresRPC1Drive",@""), NSLocalizedString(@"NotRPC1",@""), NSLocalizedString(@"IsRPC1",@""), NULL);
		
		if (button == NSAlertDefaultReturn)
		{
			[NSApp terminate:nil];
		}
		
		// Remember last version of application that issued the RPC-1 warning
		if (Version)
		{
			CFPreferencesSetAppValue(PREFRPC1WARNING, Version, kCFPreferencesCurrentApplication);
			CFPreferencesAppSynchronize(kCFPreferencesCurrentApplication);
		}
	}
	if (DidWarnAboutRPC1) CFRelease(DidWarnAboutRPC1);
	
	// 2.0: Check for a RPC-1 drive presence
	if (NoRPC1Drive())
	{ // None found. Warn user and quit
		NSRunAlertPanel(NSLocalizedString(@"NoRPC1Drive",@""), 
								 NSLocalizedString(@"RequiresRPC1Drive",@""), NSLocalizedString(@"NotRPC1",@""), NULL, NULL);
		[NSApp terminate:nil];
	}
	
	// 1.0.7: Check for needed root and setuid repairs (running as root, file owner, setuid bit etc).
	CheckForRepair();
	
	// Read the region settings and adjust display according to these.
	ReadSettings();
	
	// 1.7 set up zRegionWhenSet used as default region when quitting "5 changes left" mode
	zRegionWhenSet = (zRegion == REGIONNOTSET) ? 1 : zRegion;
	savedRegion = CFPreferencesGetAppIntegerValue(PREFLASTREGIONSET, kCFPreferencesCurrentApplication, &IsValid);
	if (IsValid) zRegionWhenSet = savedRegion;
	
	// 1.5: Quit affected applications, must be done after ReadSettings so that the DVD Player bundle name is set.
	QuitApps();
	
	// 1.6: Display the PPC only version
	Title = [[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleExecutableKey];
	if (Title) Title = [Title stringByAppendingString:@" "];
	if (Title) Title = [Title stringByAppendingString:[[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleVersionKey]];
	if (Title)
	{
		[[OKButton window] setTitle:Title];
	}
	
	// 1.5: set up the "application to launch" menu, so that names are localized automatically
	[ApplicationToLaunch setAutoenablesItems:NO];
	theErr = LSFindApplicationForInfo(kLSUnknownCreator, zDVDPlayerBundleName, NULL, NULL, &AppURL);
	if (theErr == noErr)
	{
		theErr = LSCopyDisplayNameForURL(AppURL, &AppName);
		if (theErr == noErr)
		{
			[[ApplicationToLaunch itemAtIndex:1] setTitle:(NSString *) AppName];
		}
	}
	if (theErr != noErr)
	{
		[[ApplicationToLaunch itemAtIndex:1] setTitle:(NSString *) zDVDPlayerBundleName];
		[[ApplicationToLaunch itemAtIndex:1] setEnabled:NO];
	}
	
	// Front Row is a special case
	// First, try to find out the name of the Front Row Launcher application
	theErr = LSFindApplicationForInfo(kLSUnknownCreator, FRONTROWLAUNCHERBUNDLEID, NULL, NULL, &AppURL);
	if (theErr == noErr)
	{
		theErr = LSCopyDisplayNameForURL(AppURL, &AppName);
		if (theErr == noErr)
		{
			[[ApplicationToLaunch itemAtIndex:2] setTitle:(NSString *)AppName];
		}
	}
	
	// Front Row Launcher application not found? Get the name of the CoreServices Front Row application (the background one)
	if (theErr != noErr)
	{
		theErr = LSFindApplicationForInfo(kLSUnknownCreator, FRONTROWBUNDLEID, NULL, NULL, &AppURL);
		if (theErr == noErr)
		{
			theErr = LSCopyDisplayNameForURL(AppURL, &AppName);
			if (theErr == noErr)
			{
				[[ApplicationToLaunch itemAtIndex:2] setTitle:(NSString *)AppName];
			}
		}
	}
	
	// Front Row not found or not launchable? Don't allow to select it.
	if ( (BSRemoteUIToggle == NULL) || (theErr != noErr) )
	{
		if (theErr != noErr) AppName = (CFStringRef) @"Front Row";
		[[ApplicationToLaunch itemAtIndex:2] setTitle:(NSString *)AppName];
		[[ApplicationToLaunch itemAtIndex:2] setEnabled:NO];
	}
	
	// 1.5: get the application to launch on save and quit and set menu accordingly
	zAppBundle = CFPreferencesCopyAppValue(PREFAPPLICATION, kCFPreferencesCurrentApplication);
	if (!zAppBundle) zAppBundle = NOAPPLICATIONBUNDLE; // preference not yet set, default to "no application"
	
	if (CFEqual(zAppBundle, zDVDPlayerBundleName)) MenuIndex = 1;
	else if (CFEqual(zAppBundle, FRONTROWLAUNCHERBUNDLEID)) MenuIndex = 2;
	else
	{
		zAppBundle = NOAPPLICATIONBUNDLE;
		CFPreferencesSetAppValue(PREFAPPLICATION, zAppBundle, kCFPreferencesCurrentApplication);
		CFPreferencesAppSynchronize(kCFPreferencesCurrentApplication);
		MenuIndex = 0;
	}
	
	[ApplicationToLaunch selectItemAtIndex:MenuIndex];
	
	// 1.6: corrected logic about 5 changes left/region not set
	if (zRegion ==  REGIONNOTSET) zChangesLeft = 5;
	if ( (zRegion !=  REGIONNOTSET) && (zChangesLeft == 5) ) zChangesLeft = 4;
	
	// Report current values to the user
	[Region selectCellWithTag:zRegion];
	[ChangesLeft selectCellWithTag:zChangesLeft];
	
	[[OKButton window] makeKeyAndOrderFront:self];
}



// User wants to save settings. Obey.
- (IBAction)Save:(id)sender
{
	long ChangesTag;
	long RegionTag;
	CFDataRef theData;
	SSPD theSSPD;
	OSStatus theError;
	CFURLRef AppURL;
	CFPropertyListRef savedRegionListRef;
	
	// Get values to save from the displayed window
	ChangesTag = [[ChangesLeft selectedCell] tag];
	RegionTag = [[Region selectedCell] tag];
	
	// Save to OF/EFI variables, whatever DVD Player version (if any) is installed.
	// The Region-Code is set to 0 if the delete fails, so that the same effect is achieved
	WriteNVRAM(5 - ChangesTag, RegionTag ? rotr(zRegionConstant[RegionTag - 1], 5 - ChangesTag) : 0, RegionTag ? NO : YES);
	
	// Build the SSPD property.
	theSSPD.FORM = cFORM;						// endian correction included
	theSSPD.x14  = 0x00000014;					// native endianness!
	theSSPD.DVDC = cDVDC;						// endian correction included
	theSSPD.DVDR = cDVDR;						// endian correction included
	theSSPD.x0c  = 0x0000000c;					// native endianness!
	theSSPD.SSPDRegion = RegionTag ? rotr(zRegionConstant[RegionTag - 1], 5 - ChangesTag) : 0;	// native endianness!
	theSSPD.SSPDChanges = 5 - ChangesTag;		// native endianness!
	theSSPD.x05  = 0x00000005;					// native endianness!
	
	theData = CFDataCreate(kCFAllocatorDefault, (UInt8 *) &theSSPD, sizeof(theSSPD));
	if (theData)
	{
		// Write back the SSPD structure to DVD Player's prefs.
		CFPreferencesSetAppValue(DVDPLAYERSSPD, RegionTag ? theData : NULL, zDVDPlayerBundleName);
		CFPreferencesAppSynchronize(zDVDPlayerBundleName); // Don't use CFPreferencesSynchronize as it doesn't save the modified SSPD!
	}
	
	if (!theData) // If something went wrong, tell it.
		NSRunAlertPanel(NSLocalizedString(@"SettingsNotSaved",@""), NSLocalizedString(@"DontKnowWhy",@""), NULL, NULL, NULL);
	else
		CFRelease(theData);
	
	if (!CFEqual(zAppBundle, NOAPPLICATIONBUNDLE))
	{
		if (CFEqual(zAppBundle, FRONTROWLAUNCHERBUNDLEID))
		{
			if (BSRemoteUIToggle != NULL) // From Apple's doc: you must explicitly compare it to NULL. You cannot use the negation operator (!).
				BSRemoteUIToggle();
		}
		else
		{
			theError = LSFindApplicationForInfo(kLSUnknownCreator, zAppBundle, NULL, NULL, &AppURL);
			if (theError == noErr) [[NSWorkspace sharedWorkspace] openURL:(NSURL *)AppURL];
		}
	}
	
	// 1.7, if a real region is set, remember it.
	if (RegionTag)
	{
		savedRegionListRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberLongType, &RegionTag);
		if (savedRegionListRef)
		{
			CFPreferencesSetAppValue(PREFLASTREGIONSET, savedRegionListRef, kCFPreferencesCurrentApplication);
			CFPreferencesAppSynchronize(kCFPreferencesCurrentApplication);
			CFRelease(savedRegionListRef);
		}
	}
	
	[NSApp terminate:nil];
}



// User wants to read the "read me". Obey.
- (IBAction)ReadMe:(id)sender
{
	NSString *path = [[NSBundle mainBundle] pathForResource:@"Region X" ofType:@"pdf"];
	if (path) [[NSWorkspace sharedWorkspace] openFile:path];
}



// User wants to be bored reading some information about Macs and region free drives. Obey.
- (IBAction)MacsAndRegionFree:(id)sender
{
	NSString *path = [[NSBundle mainBundle] pathForResource:@"Mac & RPC-1" ofType:@"pdf"];
	if (path) [[NSWorkspace sharedWorkspace] openFile:path];
}



// User has clicked on the picture. Direct him to my site.
- (IBAction)AboutXvi:(id)sender;
{
	NSURL *xvi = [NSURL URLWithString:@"http://xvi.rpc1.org/"];
	if (xvi) [[NSWorkspace sharedWorkspace] openURL:xvi];
}

@end
