diff --git a/Delta.xcodeproj/project.pbxproj b/Delta.xcodeproj/project.pbxproj index 29d0765..480b5d3 100644 --- a/Delta.xcodeproj/project.pbxproj +++ b/Delta.xcodeproj/project.pbxproj @@ -165,6 +165,7 @@ BFFDF03723E3BB2600931B96 /* libSnes9x.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BFFDF03623E3BB2600931B96 /* libSnes9x.a */; }; BFFDF03F23E3C28A00931B96 /* libGambatte.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BFFDF03D23E3C0F000931B96 /* libGambatte.a */; }; BFFDF04623E3D3A600931B96 /* libMupen64Plus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BFFDF04523E3D3A600931B96 /* libMupen64Plus.a */; }; + D5011C48281B6E8B00A0760B /* CharacterSet+Filename.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -358,6 +359,7 @@ BFFDF03D23E3C0F000931B96 /* libGambatte.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libGambatte.a; sourceTree = BUILT_PRODUCTS_DIR; }; BFFDF04523E3D3A600931B96 /* libMupen64Plus.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = libMupen64Plus.a; sourceTree = BUILT_PRODUCTS_DIR; }; C786AF1D2DDB6223BE2063CC /* Pods-Delta.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Delta.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Delta/Pods-Delta.debug.xcconfig"; sourceTree = ""; }; + D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CharacterSet+Filename.swift"; sourceTree = ""; }; DC866E433B3BA9AE18ABA1EC /* libPods-Delta.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Delta.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -403,6 +405,7 @@ BF647A6922FB8FCE0061D76D /* Bundle+SwizzleBundleID.swift */, BFD1EF3F2336BD8800D197CF /* UIDevice+Processor.swift */, BFE56E1823EB7BE00014FECD /* UIImage+SymbolFallback.swift */, + D5011C47281B6E8B00A0760B /* CharacterSet+Filename.swift */, ); path = Extensions; sourceTree = ""; @@ -1114,6 +1117,7 @@ BF525EEA1FF6CD12004AA849 /* DeepLink.swift in Sources */, BF8A334621A4926F00A42FD4 /* GameSyncStatusViewController.swift in Sources */, BF59427E1E09BC830051894B /* Game.swift in Sources */, + D5011C48281B6E8B00A0760B /* CharacterSet+Filename.swift in Sources */, BFE593CC21F3F8C2003412A6 /* _GameSave.swift in Sources */, BF63A1A321A4AAAE00EE8F61 /* RecordSyncStatusViewController.swift in Sources */, BFAA1FED1B8AA4FA00495943 /* Settings.swift in Sources */, diff --git a/Delta/Extensions/CharacterSet+Filename.swift b/Delta/Extensions/CharacterSet+Filename.swift new file mode 100644 index 0000000..a1119c3 --- /dev/null +++ b/Delta/Extensions/CharacterSet+Filename.swift @@ -0,0 +1,22 @@ +// +// CharacterSet+Filename.swift +// Delta +// +// Created by Riley Testut on 4/28/22. +// Copyright © 2022 Riley Testut. All rights reserved. +// + +import Foundation + +extension CharacterSet +{ + // Different than .urlPathAllowed + // Copied from https://stackoverflow.com/a/39443252 + static var urlFilenameAllowed: CharacterSet { + var illegalCharacters = CharacterSet(charactersIn: ":/") + illegalCharacters.formUnion(.newlines) + illegalCharacters.formUnion(.illegalCharacters) + illegalCharacters.formUnion(.controlCharacters) + return illegalCharacters.inverted + } +} diff --git a/Delta/Game Selection/GameCollectionViewController.swift b/Delta/Game Selection/GameCollectionViewController.swift index 9ea0251..e67720e 100644 --- a/Delta/Game Selection/GameCollectionViewController.swift +++ b/Delta/Game Selection/GameCollectionViewController.swift @@ -705,28 +705,36 @@ private extension GameCollectionViewController func share(_ game: Game) { - let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) - let symbolicURL = temporaryDirectory.appendingPathComponent(game.name + "." + game.fileURL.pathExtension) + let temporaryDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true) + + let sanitizedName = game.name.components(separatedBy: .urlFilenameAllowed.inverted).joined() + let temporaryURL = temporaryDirectory.appendingPathComponent(sanitizedName + "." + game.fileURL.pathExtension, isDirectory: false) do { try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil) - - // Create a symbolic link so we can control the file name used when sharing. + + // Make a temporary copy so we can control the filename used when sharing. // Otherwise, if we just passed in game.fileURL to UIActivityViewController, the file name would be the game's SHA1 hash. - try FileManager.default.createSymbolicLink(at: symbolicURL, withDestinationURL: game.fileURL) + try FileManager.default.copyItem(at: game.fileURL, to: temporaryURL, shouldReplace: true) } catch { - print(error) + let alertController = UIAlertController(title: NSLocalizedString("Could Not Share Game", comment: ""), error: error) + self.present(alertController, animated: true, completion: nil) + + return } let copyDeepLinkActivity = CopyDeepLinkActivity() - let activityViewController = UIActivityViewController(activityItems: [symbolicURL, game], applicationActivities: [copyDeepLinkActivity]) + let activityViewController = UIActivityViewController(activityItems: [temporaryURL, game], applicationActivities: [copyDeepLinkActivity]) activityViewController.popoverPresentationController?.sourceView = self._popoverSourceView?.superview activityViewController.popoverPresentationController?.sourceRect = self._popoverSourceView?.frame ?? .zero activityViewController.completionWithItemsHandler = { (activityType, finished, returnedItems, error) in + // Make sure the user either shared the game or cancelled before deleting temporaryDirectory. + guard finished || activityType == nil else { return } + do { try FileManager.default.removeItem(at: temporaryDirectory) @@ -736,6 +744,7 @@ private extension GameCollectionViewController print(error) } } + self.present(activityViewController, animated: true, completion: nil) } @@ -791,8 +800,7 @@ private extension GameCollectionViewController { do { - let illegalCharacterSet = CharacterSet(charactersIn: "\"\\/?<>:*|") - let sanitizedFilename = game.name.components(separatedBy: illegalCharacterSet).joined() + "." + game.gameSaveURL.pathExtension + let sanitizedFilename = game.name.components(separatedBy: .urlFilenameAllowed.inverted).joined() let temporaryURL = FileManager.default.temporaryDirectory.appendingPathComponent(sanitizedFilename) try FileManager.default.copyItem(at: game.gameSaveURL, to: temporaryURL, shouldReplace: true)