1// 2// HFPasteboardOwner.m 3// HexFiend_2 4// 5// Copyright 2008 ridiculous_fish. All rights reserved. 6// 7 8#import <HexFiend/HFPasteboardOwner.h> 9#import <HexFiend/HFController.h> 10#import <HexFiend/HFByteArray.h> 11#import <objc/message.h> 12 13NSString *const HFPrivateByteArrayPboardType = @"HFPrivateByteArrayPboardType"; 14 15@implementation HFPasteboardOwner 16 17+ (void)initialize { 18 if (self == [HFPasteboardOwner class]) { 19 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(prepareCommonPasteboardsForChangeInFileNotification:) name:HFPrepareForChangeInFileNotification object:nil]; 20 } 21} 22 23- (instancetype)initWithPasteboard:(NSPasteboard *)pboard forByteArray:(HFByteArray *)array withTypes:(NSArray *)types { 24 REQUIRE_NOT_NULL(pboard); 25 REQUIRE_NOT_NULL(array); 26 REQUIRE_NOT_NULL(types); 27 self = [super init]; 28 byteArray = [array retain]; 29 pasteboard = pboard; 30 [pasteboard declareTypes:types owner:self]; 31 32 // get notified when we're about to write a file, so that if they're overwriting a file backing part of our byte array, we can properly clear or preserve our pasteboard 33 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(changeInFileNotification:) name:HFPrepareForChangeInFileNotification object:nil]; 34 35 return self; 36} 37+ (id)ownPasteboard:(NSPasteboard *)pboard forByteArray:(HFByteArray *)array withTypes:(NSArray *)types { 38 return [[[self alloc] initWithPasteboard:pboard forByteArray:array withTypes:types] autorelease]; 39} 40 41- (void)tearDownPasteboardReferenceIfExists { 42 if (pasteboard) { 43 pasteboard = nil; 44 [[NSNotificationCenter defaultCenter] removeObserver:self name:HFPrepareForChangeInFileNotification object:nil]; 45 } 46 if (retainedSelfOnBehalfOfPboard) { 47 CFRelease(self); 48 retainedSelfOnBehalfOfPboard = NO; 49 } 50} 51 52 53+ (HFByteArray *)_unpackByteArrayFromDictionary:(NSDictionary *)byteArrayDictionary { 54 HFByteArray *result = nil; 55 if (byteArrayDictionary) { 56 NSString *uuid = byteArrayDictionary[@"HFUUID"]; 57 if ([uuid isEqual:[self uuid]]) { 58 result = (HFByteArray *)[byteArrayDictionary[@"HFByteArray"] unsignedLongValue]; 59 } 60 } 61 return result; 62} 63 64+ (HFByteArray *)unpackByteArrayFromPasteboard:(NSPasteboard *)pasteboard { 65 REQUIRE_NOT_NULL(pasteboard); 66 HFByteArray *result = [self _unpackByteArrayFromDictionary:[pasteboard propertyListForType:HFPrivateByteArrayPboardType]]; 67 return result; 68} 69 70/* Try to fix up commonly named pasteboards when a file is about to be saved */ 71+ (void)prepareCommonPasteboardsForChangeInFileNotification:(NSNotification *)notification { 72 const BOOL *cancellationPointer = [[notification userInfo][HFChangeInFileShouldCancelKey] pointerValue]; 73 if (*cancellationPointer) return; //don't do anything if someone requested cancellation 74 75 NSDictionary *userInfo = [notification userInfo]; 76 NSArray *changedRanges = userInfo[HFChangeInFileModifiedRangesKey]; 77 HFFileReference *fileReference = [notification object]; 78 NSMutableDictionary *hint = userInfo[HFChangeInFileHintKey]; 79 80 NSString * const names[] = {NSGeneralPboard, NSFindPboard, NSDragPboard}; 81 NSUInteger i; 82 for (i=0; i < sizeof names / sizeof *names; i++) { 83 NSPasteboard *pboard = [NSPasteboard pasteboardWithName:names[i]]; 84 HFByteArray *byteArray = [self unpackByteArrayFromPasteboard:pboard]; 85 if (byteArray && ! [byteArray clearDependenciesOnRanges:changedRanges inFile:fileReference hint:hint]) { 86 /* This pasteboard no longer works */ 87 [pboard declareTypes:@[] owner:nil]; 88 } 89 } 90} 91 92- (void)changeInFileNotification:(NSNotification *)notification { 93 HFASSERT(pasteboard != nil); 94 HFASSERT(byteArray != nil); 95 NSDictionary *userInfo = [notification userInfo]; 96 const BOOL *cancellationPointer = [userInfo[HFChangeInFileShouldCancelKey] pointerValue]; 97 if (*cancellationPointer) return; //don't do anything if someone requested cancellation 98 NSMutableDictionary *hint = userInfo[HFChangeInFileHintKey]; 99 100 NSArray *changedRanges = [notification userInfo][HFChangeInFileModifiedRangesKey]; 101 HFFileReference *fileReference = [notification object]; 102 if (! [byteArray clearDependenciesOnRanges:changedRanges inFile:fileReference hint:hint]) { 103 /* We can't do it */ 104 [self tearDownPasteboardReferenceIfExists]; 105 } 106} 107 108- (void)dealloc { 109 [self tearDownPasteboardReferenceIfExists]; 110 [byteArray release]; 111 [super dealloc]; 112} 113 114- (void)writeDataInBackgroundToPasteboard:(NSPasteboard *)pboard ofLength:(unsigned long long)length forType:(NSString *)type trackingProgress:(id)tracker { 115 USE(length); 116 USE(pboard); 117 USE(type); 118 USE(tracker); 119 UNIMPLEMENTED_VOID(); 120} 121 122- (void)backgroundMoveDataToPasteboard:(NSString *)type { 123 @autoreleasepool { 124 [self writeDataInBackgroundToPasteboard:pasteboard ofLength:dataAmountToCopy forType:type trackingProgress:nil]; 125 [self performSelectorOnMainThread:@selector(backgroundMoveDataFinished:) withObject:nil waitUntilDone:NO]; 126 } 127} 128 129- (void)backgroundMoveDataFinished:unused { 130 USE(unused); 131 HFASSERT(backgroundCopyOperationFinished == NO); 132 backgroundCopyOperationFinished = YES; 133 if (! didStartModalSessionForBackgroundCopyOperation) { 134 /* We haven't started the modal session, so make sure it never happens */ 135 [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(beginModalSessionForBackgroundCopyOperation:) object:nil]; 136 CFRunLoopWakeUp(CFRunLoopGetCurrent()); 137 } 138 else { 139 /* We have started the modal session, so end it. */ 140 [NSApp stopModalWithCode:0]; 141 //stopModal: won't trigger unless we post a do-nothing event 142 NSEvent *event = [NSEvent otherEventWithType:NSApplicationDefined location:NSZeroPoint modifierFlags:0 timestamp:0 windowNumber:0 context:NULL subtype:0 data1:0 data2:0]; 143 [NSApp postEvent:event atStart:NO]; 144 } 145} 146 147- (void)beginModalSessionForBackgroundCopyOperation:(id)unused { 148 USE(unused); 149 HFASSERT(backgroundCopyOperationFinished == NO); 150 HFASSERT(didStartModalSessionForBackgroundCopyOperation == NO); 151 didStartModalSessionForBackgroundCopyOperation = YES; 152} 153 154- (BOOL)moveDataWithProgressReportingToPasteboard:(NSPasteboard *)pboard forType:(NSString *)type { 155 // The -[NSRunLoop runMode:beforeDate:] call in the middle of this function can cause it to be 156 // called reentrantly, which was previously causing leaks and use-after-free crashes. For 157 // some reason this happens basically always when copying lots of data into VMware Fusion. 158 // I'm not even sure what the ideal behavior would be here, but am fairly certain that this 159 // is the best that can be done without rewriting a portion of the background copying code. 160 // TODO: Figure out what the ideal behavior should be here. 161 162 HFASSERT(pboard == pasteboard); 163 [self retain]; //resolving the pasteboard may release us, which deallocates us, which deallocates our tracker...make sure we survive through this function 164 /* Give the user a chance to request a smaller amount if it's really big */ 165 unsigned long long availableAmount = [byteArray length]; 166 unsigned long long amountToCopy = [self amountToCopyForDataLength:availableAmount stringLength:[self stringLengthForDataLength:availableAmount]]; 167 if (amountToCopy > 0) { 168 169 backgroundCopyOperationFinished = NO; 170 didStartModalSessionForBackgroundCopyOperation = NO; 171 dataAmountToCopy = amountToCopy; 172 [NSThread detachNewThreadSelector:@selector(backgroundMoveDataToPasteboard:) toTarget:self withObject:type]; 173 [self performSelector:@selector(beginModalSessionForBackgroundCopyOperation:) withObject:nil afterDelay:1.0 inModes:@[NSModalPanelRunLoopMode]]; 174 while (! backgroundCopyOperationFinished) { 175 [[NSRunLoop currentRunLoop] runMode:NSModalPanelRunLoopMode beforeDate:[NSDate distantFuture]]; 176 } 177 } 178 [self release]; 179 return YES; 180} 181 182- (void)pasteboardChangedOwner:(NSPasteboard *)pboard { 183 HFASSERT(pasteboard == pboard); 184 [self tearDownPasteboardReferenceIfExists]; 185} 186 187- (HFByteArray *)byteArray { 188 return byteArray; 189} 190 191- (void)pasteboard:(NSPasteboard *)pboard provideDataForType:(NSString *)type { 192 if (! pasteboard) { 193 /* Don't do anything, because we've torn down our pasteboard */ 194 return; 195 } 196 if ([type isEqualToString:HFPrivateByteArrayPboardType]) { 197 if (! retainedSelfOnBehalfOfPboard) { 198 retainedSelfOnBehalfOfPboard = YES; 199 CFRetain(self); 200 } 201 NSDictionary *dict = @{@"HFByteArray": @((unsigned long)byteArray), 202 @"HFUUID": [[self class] uuid]}; 203 [pboard setPropertyList:dict forType:type]; 204 } 205 else { 206 if (! [self moveDataWithProgressReportingToPasteboard:pboard forType:type]) { 207 [pboard setData:[NSData data] forType:type]; 208 } 209 } 210} 211 212- (void)setBytesPerLine:(NSUInteger)val { bytesPerLine = val; } 213- (NSUInteger)bytesPerLine { return bytesPerLine; } 214 215+ (NSString *)uuid { 216 static NSString *uuid; 217 if (! uuid) { 218 CFUUIDRef uuidRef = CFUUIDCreate(NULL); 219 uuid = (NSString *)CFUUIDCreateString(NULL, uuidRef); 220 CFRelease(uuidRef); 221 } 222 return uuid; 223} 224 225- (unsigned long long)stringLengthForDataLength:(unsigned long long)dataLength { USE(dataLength); UNIMPLEMENTED(); } 226 227- (unsigned long long)amountToCopyForDataLength:(unsigned long long)numBytes stringLength:(unsigned long long)stringLength { 228 unsigned long long dataLengthResult, stringLengthResult; 229 NSInteger alertReturn = NSIntegerMax; 230 const unsigned long long copyOption1 = MAXIMUM_PASTEBOARD_SIZE_TO_EXPORT; 231 const unsigned long long copyOption2 = MINIMUM_PASTEBOARD_SIZE_TO_WARN_ABOUT; 232 NSString *option1String = HFDescribeByteCount(copyOption1); 233 NSString *option2String = HFDescribeByteCount(copyOption2); 234 NSString* dataSizeDescription = HFDescribeByteCount(stringLength); 235 if (stringLength >= MAXIMUM_PASTEBOARD_SIZE_TO_EXPORT) { 236 NSString *option1 = [@"Copy " stringByAppendingString:option1String]; 237 NSString *option2 = [@"Copy " stringByAppendingString:option2String]; 238 alertReturn = NSRunAlertPanel(@"Large Clipboard", @"The copied data would occupy %@ if written to the clipboard. This is larger than the system clipboard supports. Do you want to copy only part of the data?", @"Cancel", option1, option2, dataSizeDescription); 239 switch (alertReturn) { 240 case NSAlertDefaultReturn: 241 default: 242 stringLengthResult = 0; 243 break; 244 case NSAlertAlternateReturn: 245 stringLengthResult = copyOption1; 246 break; 247 case NSAlertOtherReturn: 248 stringLengthResult = copyOption2; 249 break; 250 } 251 252 } 253 else if (stringLength >= MINIMUM_PASTEBOARD_SIZE_TO_WARN_ABOUT) { 254 NSString *option1 = [@"Copy " stringByAppendingString:HFDescribeByteCount(stringLength)]; 255 NSString *option2 = [@"Copy " stringByAppendingString:HFDescribeByteCount(copyOption2)]; 256 alertReturn = NSRunAlertPanel(@"Large Clipboard", @"The copied data would occupy %@ if written to the clipboard. Performing this copy may take a long time. Do you want to copy only part of the data?", @"Cancel", option1, option2, dataSizeDescription); 257 switch (alertReturn) { 258 case NSAlertDefaultReturn: 259 default: 260 stringLengthResult = 0; 261 break; 262 case NSAlertAlternateReturn: 263 stringLengthResult = stringLength; 264 break; 265 case NSAlertOtherReturn: 266 stringLengthResult = copyOption2; 267 break; 268 } 269 } 270 else { 271 /* Small enough to copy it all */ 272 stringLengthResult = stringLength; 273 } 274 275 /* Convert from string length to data length */ 276 if (stringLengthResult == stringLength) { 277 dataLengthResult = numBytes; 278 } 279 else { 280 unsigned long long divisor = stringLength / numBytes; 281 dataLengthResult = stringLengthResult / divisor; 282 } 283 284 return dataLengthResult; 285} 286 287@end 288