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