diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 0217953..e5e975a 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 536CFDD2263C94DE00026CE0 /* SignedInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536CFDD1263C94DE00026CE0 /* SignedInView.swift */; }; + 536CFDD4263C9A8000026CE0 /* XcodesSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536CFDD3263C9A8000026CE0 /* XcodesSheet.swift */; }; 63EAA4EB259944450046AB8F /* ProgressButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63EAA4EA259944450046AB8F /* ProgressButton.swift */; }; CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */; }; CA2518EC25A7FF2B00F08414 /* AppStateUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA2518EB25A7FF2B00F08414 /* AppStateUpdateTests.swift */; }; @@ -158,6 +160,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 536CFDD1263C94DE00026CE0 /* SignedInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignedInView.swift; sourceTree = ""; }; + 536CFDD3263C9A8000026CE0 /* XcodesSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodesSheet.swift; sourceTree = ""; }; 63EAA4EA259944450046AB8F /* ProgressButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressButton.swift; sourceTree = ""; }; CA11E7B92598476C00D2EE1C /* XcodeCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XcodeCommands.swift; sourceTree = ""; }; CA2518EB25A7FF2B00F08414 /* AppStateUpdateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateUpdateTests.swift; sourceTree = ""; }; @@ -311,6 +315,7 @@ CAC281CC259F97FA00B8AB0B /* ObservingProgressIndicator.swift */, 63EAA4EA259944450046AB8F /* ProgressButton.swift */, CA452BAF259FD9770072DFA4 /* ProgressIndicator.swift */, + 536CFDD3263C9A8000026CE0 /* XcodesSheet.swift */, ); path = Common; sourceTree = ""; @@ -386,6 +391,7 @@ CAA1CB44255A5B60003FD669 /* SignIn2FAView.swift */, CAA1CB48255A5C97003FD669 /* SignInSMSView.swift */, CAA1CB4C255A5CFD003FD669 /* SignInPhoneListView.swift */, + 536CFDD1263C94DE00026CE0 /* SignedInView.swift */, ); path = SignIn; sourceTree = ""; @@ -761,6 +767,7 @@ CAFBDC4E2599B33D003DCC5A /* MainToolbar.swift in Sources */, CA11E7BA2598476C00D2EE1C /* XcodeCommands.swift in Sources */, CAA8589B25A2B83000ACF8C0 /* Aria2CError.swift in Sources */, + 536CFDD2263C94DE00026CE0 /* SignedInView.swift in Sources */, CABFAA492593162500380FEE /* Bundle+InfoPlistValues.swift in Sources */, CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */, CAE4248C259A68B800B8B246 /* Optional+IsNotNil.swift in Sources */, @@ -821,6 +828,7 @@ CABFA9CC2592EEEA00380FEE /* Path+.swift in Sources */, CAD2E7A22449574E00113D76 /* XcodesApp.swift in Sources */, 63EAA4EB259944450046AB8F /* ProgressButton.swift in Sources */, + 536CFDD4263C9A8000026CE0 /* XcodesSheet.swift in Sources */, E89342FA25EDCC17007CF557 /* NotificationManager.swift in Sources */, CA5D781E257365D6008EDE9D /* PinCodeTextView.swift in Sources */, CA39711924495F0E00AFFB77 /* AppStoreButtonStyle.swift in Sources */, diff --git a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved deleted file mode 100644 index c92db7b..0000000 --- a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ /dev/null @@ -1,88 +0,0 @@ -{ - "object": { - "pins": [ - { - "package": "CombineExpectations", - "repositoryURL": "https://github.com/groue/CombineExpectations", - "state": { - "branch": null, - "revision": "989a92221899929ab8347a5878aa2b16db8b81ca", - "version": "0.6.0" - } - }, - { - "package": "XcodeReleases", - "repositoryURL": "https://github.com/xcodereleases/data", - "state": { - "branch": null, - "revision": "b47228c688b608e34b3b84079ab6052a24c7a981", - "version": null - } - }, - { - "package": "ErrorHandling", - "repositoryURL": "https://github.com/RobotsAndPencils/ErrorHandling", - "state": { - "branch": null, - "revision": "7be837fcb515447c0776805c3288fb7d5181ec68", - "version": "0.1.0" - } - }, - { - "package": "KeychainAccess", - "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess", - "state": { - "branch": null, - "revision": "8d33ffd6f74b3bcfc99af759d4204c6395a3f918", - "version": "3.2.1" - } - }, - { - "package": "LegibleError", - "repositoryURL": "https://github.com/mxcl/LegibleError", - "state": { - "branch": null, - "revision": "909e9bab3ded97350b28a5ab41dd745dd8aa9710", - "version": "1.0.4" - } - }, - { - "package": "Path.swift", - "repositoryURL": "https://github.com/mxcl/Path.swift", - "state": { - "branch": null, - "revision": "dac007e907a4f4c565cfdc55a9ce148a761a11d5", - "version": "0.16.3" - } - }, - { - "package": "Sparkle", - "repositoryURL": "https://github.com/sparkle-project/Sparkle/", - "state": { - "branch": null, - "revision": "891afd44c7075e699924ed9b81d8dc94a5111dfd", - "version": "1.24.0-spm" - } - }, - { - "package": "SwiftSoup", - "repositoryURL": "https://github.com/scinfu/SwiftSoup", - "state": { - "branch": null, - "revision": "aeb5b4249c273d1783a5299e05be1b26e061ea81", - "version": "2.0.0" - } - }, - { - "package": "Version", - "repositoryURL": "https://github.com/mxcl/Version", - "state": { - "branch": null, - "revision": "087c91fedc110f9f833b14ef4c32745dabca8913", - "version": "1.0.3" - } - } - ] - }, - "version": 1 -} diff --git a/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift b/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift index f04f069..462286d 100644 --- a/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift +++ b/Xcodes/AppleAPI/Sources/AppleAPI/Client.swift @@ -36,6 +36,10 @@ public class Client { case 401: return Fail(error: AuthenticationError.invalidUsernameOrPassword(username: accountName)) .eraseToAnyPublisher() + case 403: + let errorMessage = responseBody.serviceErrors?.first?.description.replacingOccurrences(of: "-20209: ", with: "") ?? "" + return Fail(error: AuthenticationError.accountLocked(errorMessage)) + .eraseToAnyPublisher() case 409: return self.handleTwoStepOrFactor(data: data, response: response, serviceKey: serviceKey) case 412 where Client.authTypes.contains(responseBody.authType ?? ""): @@ -180,6 +184,7 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable { case appleIDAndPrivacyAcknowledgementRequired case accountUsesTwoStepAuthentication case accountUsesUnknownAuthenticationKind(String?) + case accountLocked(String) case badStatusCode(statusCode: Int, data: Data, response: HTTPURLResponse) public var errorDescription: String? { @@ -203,6 +208,8 @@ public enum AuthenticationError: Swift.Error, LocalizedError, Equatable { return "Received a response from Apple that indicates this account has two-step authentication enabled. xcodes currently only supports the newer two-factor authentication, though. Please consider upgrading to two-factor authentication, or explain why this isn't an option for you by making a new feature request in the Help menu." case .accountUsesUnknownAuthenticationKind: return "Received a response from Apple that indicates this account has two-step or two-factor authentication enabled, but xcodes is unsure how to handle this response. If you continue to have problems, please submit a bug report in the Help menu." + case let .accountLocked(message): + return message case let .badStatusCode(statusCode, _, _): return "Received an unexpected status code: \(statusCode). If you continue to have problems, please submit a bug report in the Help menu." } diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 6ea8b2c..f928faa 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -41,7 +41,7 @@ class AppState: ObservableObject { } @Published var updatePublisher: AnyCancellable? var isUpdating: Bool { updatePublisher != nil } - @Published var presentingSignInAlert = false + @Published var presentedSheet: XcodesSheet? = nil @Published var isProcessingAuthRequest = false @Published var secondFactorData: SecondFactorData? @Published var xcodeBeingConfirmedForUninstallation: Xcode? @@ -68,6 +68,14 @@ class AppState: ObservableObject { var dataSource: DataSource { Current.defaults.string(forKey: "dataSource").flatMap(DataSource.init(rawValue:)) ?? .default } + + var savedUsername: String? { + Current.defaults.string(forKey: "username") + } + + var hasSavedUsername: Bool { + savedUsername != nil + } // MARK: - Init @@ -97,7 +105,7 @@ class AppState: ObservableObject { .handleEvents(receiveCompletion: { completion in if case .failure = completion { self.authenticationState = .unauthenticated - self.presentingSignInAlert = true + self.presentedSheet = .signIn } }) .eraseToAnyPublisher() @@ -107,7 +115,7 @@ class AppState: ObservableObject { validateSession() .catch { (error) -> AnyPublisher in guard - let username = Current.defaults.string(forKey: "username"), + let username = self.savedUsername, let password = try? Current.keychain.getString(username) else { return Fail(error: error) @@ -122,6 +130,7 @@ class AppState: ObservableObject { } func signIn(username: String, password: String) { + authError = nil signIn(username: username, password: password) .sink( receiveCompletion: { _ in }, @@ -150,12 +159,12 @@ class AppState: ObservableObject { } func handleTwoFactorOption(_ option: TwoFactorOption, authOptions: AuthOptionsResponse, serviceKey: String, sessionID: String, scnt: String) { - self.presentingSignInAlert = false self.secondFactorData = SecondFactorData( option: option, authOptions: authOptions, sessionData: AppleSessionData(serviceKey: serviceKey, sessionID: sessionID, scnt: scnt) ) + self.presentedSheet = .twoFactor } func requestSMS(to trustedPhoneNumber: AuthOptionsResponse.TrustedPhoneNumber, authOptions: AuthOptionsResponse, sessionData: AppleSessionData) { @@ -201,7 +210,7 @@ class AppState: ObservableObject { switch completion { case let .failure(error): if case .invalidUsernameOrPassword = error as? AuthenticationError, - let username = Current.defaults.string(forKey: "username") { + let username = savedUsername { // remove any keychain password if we fail to log with an invalid username or password so it doesn't try again. try? Current.keychain.remove(username) Current.defaults.removeObject(forKey: "username") @@ -212,7 +221,7 @@ class AppState: ObservableObject { case .finished: switch self.authenticationState { case .authenticated, .unauthenticated: - self.presentingSignInAlert = false + self.presentedSheet = nil self.secondFactorData = nil case let .waitingForSecondFactor(option, authOptions, sessionData): self.handleTwoFactorOption(option, authOptions: authOptions, serviceKey: sessionData.serviceKey, sessionID: sessionData.sessionID, scnt: sessionData.scnt) @@ -221,11 +230,10 @@ class AppState: ObservableObject { } func signOut() { - let username = Current.defaults.string(forKey: "username") - Current.defaults.removeObject(forKey: "username") - if let username = username { + if let username = savedUsername { try? Current.keychain.remove(username) } + Current.defaults.removeObject(forKey: "username") AppleAPI.Current.network.session.configuration.httpCookieStorage?.removeCookies(since: .distantPast) authenticationState = .unauthenticated } @@ -331,7 +339,10 @@ class AppState: ObservableObject { receiveCompletion: { [unowned self] completion in self.installationPublishers[id] = nil if case let .failure(error) = completion { - self.error = error + // Prevent setting the app state error if it is an invalid session, we will present the sign in view instead + if error as? AuthenticationError != .invalidSession { + self.error = error + } if let index = self.allXcodes.firstIndex(where: { $0.id == id }) { self.allXcodes[index].installState = .notInstalled } diff --git a/Xcodes/Frontend/Common/XcodesSheet.swift b/Xcodes/Frontend/Common/XcodesSheet.swift new file mode 100644 index 0000000..0a39add --- /dev/null +++ b/Xcodes/Frontend/Common/XcodesSheet.swift @@ -0,0 +1,8 @@ +import Foundation + +enum XcodesSheet: Identifiable { + case signIn + case twoFactor + + var id: Int { hashValue } +} diff --git a/Xcodes/Frontend/MainWindow.swift b/Xcodes/Frontend/MainWindow.swift index d003227..954b221 100644 --- a/Xcodes/Frontend/MainWindow.swift +++ b/Xcodes/Frontend/MainWindow.swift @@ -38,9 +38,15 @@ struct MainWindow: View { .navigationSubtitle(subtitleText) .frame(minWidth: 600, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity) .emittingError($appState.error, recoveryHandler: { _ in }) - .sheet(isPresented: $appState.secondFactorData.isNotNil) { - secondFactorView(appState.secondFactorData!) - .environmentObject(appState) + .sheet(item: $appState.presentedSheet) { sheet in + switch sheet { + case .signIn: + signInView() + .environmentObject(appState) + case .twoFactor: + secondFactorView(appState.secondFactorData!) + .environmentObject(appState) + } } // This overlay is only here to work around the one-alert-per-view limitation .overlay( @@ -111,6 +117,25 @@ struct MainWindow: View { SignInPhoneListView(isPresented: $appState.secondFactorData.isNotNil, authOptions: secondFactorData.authOptions, sessionData: secondFactorData.sessionData) } } + + @ViewBuilder + private func signInView() -> some View { + if appState.authenticationState == .authenticated { + VStack { + SignedInView() + .padding(32) + HStack { + Spacer() + Button("Close") { appState.presentedSheet = nil } + .keyboardShortcut(.cancelAction) + } + } + .padding() + } else { + SignInCredentialsView() + .frame(width: 400) + } + } } struct MainWindow_Previews: PreviewProvider { diff --git a/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift b/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift index 06f8529..35d5eaf 100644 --- a/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift +++ b/Xcodes/Frontend/Preferences/GeneralPreferencePane.swift @@ -7,28 +7,13 @@ struct GeneralPreferencePane: View { var body: some View { VStack(alignment: .leading) { GroupBox(label: Text("Apple ID")) { - // If we have saved a username then we will show it here, - // even if we don't have a valid session right now, - // because we should be able to get a valid session if needed with the password in the keychain - // and a 2FA code from the user. - // Note that AppState.authenticationState is not necessarily .authenticated in this case, though. - if let username = Current.defaults.string(forKey: "username") { - HStack(alignment:.top, spacing: 10) { - Text(username) - Button("Sign Out", action: appState.signOut) - } - .frame(maxWidth: .infinity, alignment: .leading) - } else { - Button("Sign In", action: { self.appState.presentingSignInAlert = true }) - .frame(maxWidth: .infinity, alignment: .leading) - } + if appState.authenticationState == .authenticated { + SignedInView() + } else { + Button("Sign In", action: { self.appState.presentedSheet = .signIn }) + } } .groupBoxStyle(PreferencesGroupBoxStyle()) - .sheet(isPresented: $appState.presentingSignInAlert) { - SignInCredentialsView(isPresented: $appState.presentingSignInAlert) - .environmentObject(appState) - } - Divider() GroupBox(label: Text("Notifications")) { diff --git a/Xcodes/Frontend/SignIn/SignInCredentialsView.swift b/Xcodes/Frontend/SignIn/SignInCredentialsView.swift index accc54a..593f735 100644 --- a/Xcodes/Frontend/SignIn/SignInCredentialsView.swift +++ b/Xcodes/Frontend/SignIn/SignInCredentialsView.swift @@ -2,7 +2,6 @@ import SwiftUI struct SignInCredentialsView: View { @EnvironmentObject var appState: AppState - @Binding var isPresented: Bool @State private var username: String = "" @State private var password: String = "" @@ -15,36 +14,49 @@ struct SignInCredentialsView: View { Text("Apple ID:") .frame(minWidth: 100, alignment: .trailing) TextField("example@icloud.com", text: $username) - .frame(width: 250) } HStack { Text("Password:") .frame(minWidth: 100, alignment: .trailing) SecureField("Required", text: $password) - .frame(width: 250) + } + if appState.authError != nil { + HStack { + Text("") + .frame(minWidth: 100) + Text(appState.authError?.legibleLocalizedDescription ?? "") + .fixedSize(horizontal: false, vertical: true) + .foregroundColor(.red) + } } HStack { Spacer() - Button("Cancel") { isPresented = false } - .keyboardShortcut(.cancelAction) - ProgressButton(isInProgress: appState.isProcessingAuthRequest, - action: { appState.signIn(username: username, password: password) }) { - Text("Next") + Button("Cancel") { + appState.authError = nil + appState.presentedSheet = nil } + .keyboardShortcut(.cancelAction) + ProgressButton( + isInProgress: appState.isProcessingAuthRequest, + action: { appState.signIn(username: username, password: password) }, + label: { + Text("Next") + } + ) .disabled(username.isEmpty || password.isEmpty) .keyboardShortcut(.defaultAction) } .frame(height: 25) } .padding() - .emittingError($appState.authError, recoveryHandler: { _ in }) } } struct SignInCredentialsView_Previews: PreviewProvider { static var previews: some View { - SignInCredentialsView(isPresented: .constant(true)) + SignInCredentialsView() .environmentObject(AppState()) + .previewLayout(.sizeThatFits) } } diff --git a/Xcodes/Frontend/SignIn/SignedInView.swift b/Xcodes/Frontend/SignIn/SignedInView.swift new file mode 100644 index 0000000..1cf5e5d --- /dev/null +++ b/Xcodes/Frontend/SignIn/SignedInView.swift @@ -0,0 +1,24 @@ +import SwiftUI + +struct SignedInView: View { + @EnvironmentObject var appState: AppState + + private var username: String { + appState.savedUsername ?? "" + } + + var body: some View { + HStack(alignment:.top, spacing: 10) { + Text(username) + Button("Sign Out", action: appState.signOut) + } + .frame(maxWidth: .infinity, alignment: .leading) + } +} + +struct SignedInView_Previews: PreviewProvider { + static var previews: some View { + SignedInView() + .previewLayout(.sizeThatFits) + } +} diff --git a/Xcodes/Frontend/XcodeList/MainToolbar.swift b/Xcodes/Frontend/XcodeList/MainToolbar.swift index 392ae32..bedf3be 100644 --- a/Xcodes/Frontend/XcodeList/MainToolbar.swift +++ b/Xcodes/Frontend/XcodeList/MainToolbar.swift @@ -13,6 +13,11 @@ struct MainToolbarModifier: ViewModifier { private var toolbar: some ToolbarContent { ToolbarItemGroup(placement: .status) { + Button(action: { appState.presentedSheet = .signIn }, label: { + Label("Login", systemImage: "person.circle") + }) + .help("Login") + ProgressButton( isInProgress: appState.isUpdating, action: appState.update diff --git a/Xcodes/Resources/Licenses.rtf b/Xcodes/Resources/Licenses.rtf index 2abb775..58f4a3d 100644 --- a/Xcodes/Resources/Licenses.rtf +++ b/Xcodes/Resources/Licenses.rtf @@ -363,6 +363,73 @@ SOFTWARE.\ \ \ +\fs34 Sparkle\ +\ + +\fs26 Copyright (c) 2006-2013 Andy Matuschak.\ +Copyright (c) 2009-2013 Elgato Systems GmbH.\ +Copyright (c) 2011-2014 Kornel Lesi\uc0\u324 ski.\ +Copyright (c) 2015-2017 Mayur Pawashe.\ +Copyright (c) 2014 C.W. Betts.\ +Copyright (c) 2014 Petroules Corporation.\ +Copyright (c) 2014 Big Nerd Ranch.\ +All rights reserved.\ +\ +Permission is hereby granted, free of charge, to any person obtaining a copy of\ +this software and associated documentation files (the "Software"), to deal in\ +the Software without restriction, including without limitation the rights to\ +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\ +the Software, and to permit persons to whom the Software is furnished to do so,\ +subject to the following conditions:\ +\ +The above copyright notice and this permission notice shall be included in all\ +copies or substantial portions of the Software.\ +\ +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\ +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\ +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\ +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\ +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\ +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\ +\ +=================\ +EXTERNAL LICENSES\ +=================\ +\ +bspatch.c and bsdiff.c, from bsdiff 4.3 :\ + Copyright (c) 2003-2005 Colin Percival.\ +\ +sais.c and sais.c, from sais-lite (2010/08/07) :\ + Copyright (c) 2008-2010 Yuta Mori.\ +\ +SUDSAVerifier.m:\ + Copyright (c) 2011 Mark Hamlin.\ +\ +All rights reserved.\ +\ +Redistribution and use in source and binary forms, with or without\ +modification, are permitted providing that the following conditions\ +are met:\ +1. Redistributions of source code must retain the above copyright\ + notice, this list of conditions and the following disclaimer.\ +2. Redistributions in binary form must reproduce the above copyright\ + notice, this list of conditions and the following disclaimer in the\ + documentation and/or other materials provided with the distribution.\ +\ +THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR\ +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\ +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\ +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY\ +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\ +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS\ +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\ +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,\ +STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING\ +IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\ +POSSIBILITY OF SUCH DAMAGE.\ +\ +\ + \fs34 LegibleError\ \