From b18839d1b922bcf00b5ada21e1748b6d78c6773f Mon Sep 17 00:00:00 2001 From: Joe Wreschnig Date: Thu, 7 Mar 2013 02:06:48 +0100 Subject: [PATCH] Provide file promises from the mapping list. Perform various file sanitization in doing so. --- Enjoyable.xcodeproj/project.pbxproj | 12 +++++++++ Enjoyable_Prefix.pch | 2 ++ NJMapping.h | 1 + NJMapping.m | 11 ++++++++ NJMappingsController.m | 39 +++++++++++++++++++---------- NSFileManager+UniqueNames.h | 26 +++++++++++++++++++ NSFileManager+UniqueNames.m | 31 +++++++++++++++++++++++ NSString+FixFilename.h | 19 ++++++++++++++ NSString+FixFilename.m | 26 +++++++++++++++++++ 9 files changed, 154 insertions(+), 13 deletions(-) create mode 100644 NSFileManager+UniqueNames.h create mode 100644 NSFileManager+UniqueNames.m create mode 100644 NSString+FixFilename.h create mode 100644 NSString+FixFilename.m diff --git a/Enjoyable.xcodeproj/project.pbxproj b/Enjoyable.xcodeproj/project.pbxproj index aef1ed2..e8badf9 100644 --- a/Enjoyable.xcodeproj/project.pbxproj +++ b/Enjoyable.xcodeproj/project.pbxproj @@ -35,6 +35,8 @@ EE1D7C9616E0ECCF00B000EB /* NSError+Description.m in Sources */ = {isa = PBXBuildFile; fileRef = EE1D7C9516E0ECCF00B000EB /* NSError+Description.m */; }; EE96929416E54B480054A3C8 /* NSMenu+RepresentedObjectAccessors.m in Sources */ = {isa = PBXBuildFile; fileRef = EE96929316E54B480054A3C8 /* NSMenu+RepresentedObjectAccessors.m */; }; EEAA9CE116E808E600256B64 /* NSMutableArray+MoveObject.m in Sources */ = {isa = PBXBuildFile; fileRef = EEAA9CE016E808E600256B64 /* NSMutableArray+MoveObject.m */; }; + EEAA9CE416E816C800256B64 /* NSFileManager+UniqueNames.m in Sources */ = {isa = PBXBuildFile; fileRef = EEAA9CE316E816C800256B64 /* NSFileManager+UniqueNames.m */; }; + EEAA9CE716E81C8400256B64 /* NSString+FixFilename.m in Sources */ = {isa = PBXBuildFile; fileRef = EEAA9CE616E81C8400256B64 /* NSString+FixFilename.m */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -92,6 +94,10 @@ EE96929316E54B480054A3C8 /* NSMenu+RepresentedObjectAccessors.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSMenu+RepresentedObjectAccessors.m"; sourceTree = ""; }; EEAA9CDF16E808E600256B64 /* NSMutableArray+MoveObject.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSMutableArray+MoveObject.h"; sourceTree = ""; }; EEAA9CE016E808E600256B64 /* NSMutableArray+MoveObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSMutableArray+MoveObject.m"; sourceTree = ""; }; + EEAA9CE216E816C800256B64 /* NSFileManager+UniqueNames.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSFileManager+UniqueNames.h"; sourceTree = ""; }; + EEAA9CE316E816C800256B64 /* NSFileManager+UniqueNames.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSFileManager+UniqueNames.m"; sourceTree = ""; }; + EEAA9CE516E81C8400256B64 /* NSString+FixFilename.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+FixFilename.h"; sourceTree = ""; }; + EEAA9CE616E81C8400256B64 /* NSString+FixFilename.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+FixFilename.m"; sourceTree = ""; }; EEF86B7316E2241000674B87 /* NJInputPathElement.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NJInputPathElement.h; sourceTree = ""; }; EEF86B7416E298CD00674B87 /* NJEvents.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NJEvents.h; sourceTree = ""; }; /* End PBXFileReference section */ @@ -224,6 +230,8 @@ EE1D5F8B16E403D600749C36 /* Categories */ = { isa = PBXGroup; children = ( + EEAA9CE216E816C800256B64 /* NSFileManager+UniqueNames.h */, + EEAA9CE316E816C800256B64 /* NSFileManager+UniqueNames.m */, EE1D7C9416E0ECCF00B000EB /* NSError+Description.h */, EE1D7C9516E0ECCF00B000EB /* NSError+Description.m */, EE1D7C9016E01E7000B000EB /* NSView+FirstResponder.h */, @@ -232,6 +240,8 @@ EE96929316E54B480054A3C8 /* NSMenu+RepresentedObjectAccessors.m */, EEAA9CDF16E808E600256B64 /* NSMutableArray+MoveObject.h */, EEAA9CE016E808E600256B64 /* NSMutableArray+MoveObject.m */, + EEAA9CE516E81C8400256B64 /* NSString+FixFilename.h */, + EEAA9CE616E81C8400256B64 /* NSString+FixFilename.m */, ); name = Categories; sourceTree = ""; @@ -343,6 +353,8 @@ EE1D7C9616E0ECCF00B000EB /* NSError+Description.m in Sources */, EE96929416E54B480054A3C8 /* NSMenu+RepresentedObjectAccessors.m in Sources */, EEAA9CE116E808E600256B64 /* NSMutableArray+MoveObject.m in Sources */, + EEAA9CE416E816C800256B64 /* NSFileManager+UniqueNames.m in Sources */, + EEAA9CE716E81C8400256B64 /* NSString+FixFilename.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Enjoyable_Prefix.pch b/Enjoyable_Prefix.pch index 4f50186..4845e9d 100644 --- a/Enjoyable_Prefix.pch +++ b/Enjoyable_Prefix.pch @@ -12,3 +12,5 @@ #import "NSMenu+RepresentedObjectAccessors.h" #import "NSView+FirstResponder.h" #import "NSMutableArray+MoveObject.h" +#import "NSFileManager+UniqueNames.h" +#import "NSString+FixFilename.h" diff --git a/NJMapping.h b/NJMapping.h index f997fa8..069f132 100644 --- a/NJMapping.h +++ b/NJMapping.h @@ -18,5 +18,6 @@ - (NJOutput *)objectForKeyedSubscript:(NJInput *)input; - (void)setObject:(NJOutput *)output forKeyedSubscript:(NJInput *)input; - (NSDictionary *)serialize; +- (BOOL)writeToURL:(NSURL *)url error:(NSError **)error; @end diff --git a/NJMapping.m b/NJMapping.m index 4e7c4b2..7df321c 100644 --- a/NJMapping.m +++ b/NJMapping.m @@ -42,4 +42,15 @@ return @{ @"name": _name, @"entries": entries }; } +- (BOOL)writeToURL:(NSURL *)url error:(NSError **)error { + [NSProcessInfo.processInfo disableSuddenTermination]; + NSDictionary *serialization = [self serialize]; + NSData *json = [NSJSONSerialization dataWithJSONObject:serialization + options:NSJSONWritingPrettyPrinted + error:error]; + BOOL success = json && [json writeToURL:url options:NSDataWritingAtomic error:error]; + [NSProcessInfo.processInfo enableSuddenTermination]; + return success; +} + @end diff --git a/NJMappingsController.m b/NJMappingsController.m index cb5baf1..ff9b42c 100644 --- a/NJMappingsController.m +++ b/NJMappingsController.m @@ -18,6 +18,7 @@ @implementation NJMappingsController { NSMutableArray *_mappings; NJMapping *manualMapping; + NSString *draggingName; } - (id)init { @@ -32,6 +33,7 @@ - (void)awakeFromNib { [tableView registerForDraggedTypes:@[PB_ROW]]; + [tableView setDraggingSourceOperationMask:NSDragOperationCopy forLocal:NO]; } - (NJMapping *)objectForKeyedSubscript:(NSString *)name { @@ -297,16 +299,8 @@ if (result != NSFileHandlingPanelOKButton) return; [panel close]; - [NSProcessInfo.processInfo disableSuddenTermination]; NSError *error; - NSDictionary *serialization = [mapping serialize]; - NSData *json = [NSJSONSerialization dataWithJSONObject:serialization - options:NSJSONWritingPrettyPrinted - error:&error]; - if (!error) - [json writeToURL:panel.URL options:NSDataWritingAtomic error:&error]; - - [NSProcessInfo.processInfo enableSuddenTermination]; + [mapping writeToURL:panel.URL error:&error]; if (error) { [window presentError:error modalForWindow:window @@ -318,7 +312,9 @@ } - (IBAction)mappingPressed:(id)sender { - [popover showRelativeToRect:popoverActivate.bounds ofView:popoverActivate preferredEdge:NSMinXEdge]; + [popover showRelativeToRect:popoverActivate.bounds + ofView:popoverActivate + preferredEdge:NSMinXEdge]; } - (void)popoverWillShow:(NSNotification *)notification { @@ -374,17 +370,34 @@ } } -- (BOOL)tableView:(NSTableView *)tableView +- (NSArray *)tableView:(NSTableView *)tableView_ +namesOfPromisedFilesDroppedAtDestination:(NSURL *)dropDestination +forDraggedRowsWithIndexes:(NSIndexSet *)indexSet { + NJMapping *toSave = self[indexSet.firstIndex]; + NSString *filename = [[toSave.name stringByFixingPathComponent] + stringByAppendingPathExtension:@"enjoyable"]; + NSURL *dst = [dropDestination URLByAppendingPathComponent:filename]; + dst = [NSFileManager.defaultManager generateUniqueURLWithBase:dst]; + NSError *error; + if (![toSave writeToURL:dst error:&error]) { + [tableView_ presentError:error]; + return @[]; + } else { + return @[dst.lastPathComponent]; + } +} + +- (BOOL)tableView:(NSTableView *)tableView_ writeRowsWithIndexes:(NSIndexSet *)rowIndexes toPasteboard:(NSPasteboard *)pboard { if (rowIndexes.count == 1 && rowIndexes.firstIndex != 0) { - [pboard declareTypes:@[PB_ROW] owner:nil]; + [pboard declareTypes:@[PB_ROW, NSFilesPromisePboardType] owner:nil]; [pboard setString:@(rowIndexes.firstIndex).stringValue forType:PB_ROW]; + [pboard setPropertyList:@[@"enjoyable"] forType:NSFilesPromisePboardType]; return YES; } else { return NO; } - } @end diff --git a/NSFileManager+UniqueNames.h b/NSFileManager+UniqueNames.h new file mode 100644 index 0000000..82c57d4 --- /dev/null +++ b/NSFileManager+UniqueNames.h @@ -0,0 +1,26 @@ +// +// NSFileManager+UniqueNames.h +// Enjoyable +// +// Created by Joe Wreschnig on 3/7/13. +// +// + +#import + +@interface NSFileManager (UniqueNames) + +- (NSURL *)generateUniqueURLWithBase:(NSURL *)canonical; + // Generate a probably-unique URL by trying sequential indices, e.g. + // file://Test.txt + // file://Test (1).txt + // file://Test (2).txt + // and so on. + // + // The URL is only probably unique. It is subject to the usual + // race conditions associated with generating a filename before + // actually opening it. It also does not check remote resources, + // as it operates synchronously. Finally, it gives up after 10,000 + // indices. + +@end diff --git a/NSFileManager+UniqueNames.m b/NSFileManager+UniqueNames.m new file mode 100644 index 0000000..c7eb768 --- /dev/null +++ b/NSFileManager+UniqueNames.m @@ -0,0 +1,31 @@ +// +// NSFileManager+UniqueNames.m +// Enjoyable +// +// Created by Joe Wreschnig on 3/7/13. +// +// + +#import "NSFileManager+UniqueNames.h" + +@implementation NSFileManager (UniqueNames) + +- (NSURL *)generateUniqueURLWithBase:(NSURL *)canonical { + // Punt for cases that are just too hard. + if (!canonical.isFileURL) + return canonical; + + NSString *trying = canonical.path; + NSString *dirname = [trying stringByDeletingLastPathComponent]; + NSString *basename = [trying.lastPathComponent stringByDeletingPathExtension]; + NSString *extension = trying.pathExtension; + int index = 1; + while ([self fileExistsAtPath:trying] && index < 10000) { + NSString *indexName = [NSString stringWithFormat:@"%@ (%d)", basename, index++]; + indexName = [indexName stringByAppendingPathExtension:extension]; + trying = [dirname stringByAppendingPathComponent:indexName]; + } + return [NSURL fileURLWithPath:trying]; +} + +@end diff --git a/NSString+FixFilename.h b/NSString+FixFilename.h new file mode 100644 index 0000000..0850e74 --- /dev/null +++ b/NSString+FixFilename.h @@ -0,0 +1,19 @@ +// +// NSString+FixFilename.h +// Enjoyable +// +// Created by Joe Wreschnig on 3/7/13. +// +// + +#import + +@interface NSString (FixFilename) + +- (NSString *)stringByFixingPathComponent; + // Does various operations to make this string suitable for use as + // a single path component of a normal filename. Removes / and : + // characters and prepends a _ to avoid leading .s or an empty + // name. + +@end diff --git a/NSString+FixFilename.m b/NSString+FixFilename.m new file mode 100644 index 0000000..fc8ba57 --- /dev/null +++ b/NSString+FixFilename.m @@ -0,0 +1,26 @@ +// +// NSString+FixFilename.m +// Enjoyable +// +// Created by Joe Wreschnig on 3/7/13. +// +// + +#import "NSString+FixFilename.h" + +@implementation NSString (FixFilename) + +- (NSString *)stringByFixingPathComponent { + static NSCharacterSet *invalid; + if (!invalid) + invalid = [NSCharacterSet characterSetWithCharactersInString:@"/:"]; + NSArray *parts = [self componentsSeparatedByCharactersInSet:invalid]; + NSString *newName = [parts componentsJoinedByString:@""]; + if (!newName.length) + return @"_"; + if ([newName characterAtIndex:0] == '.') + newName = [@"_" stringByAppendingString:newName]; + return newName; +} + +@end -- 2.20.1