18
18
19
19
package airsquared .blobsaver .app ;
20
20
21
+ import airsquared .blobsaver .app .LibimobiledeviceUtil .LibimobiledeviceException ;
21
22
import picocli .CommandLine ;
22
23
import picocli .CommandLine .*;
23
24
import picocli .CommandLine .Model .ArgSpec ;
27
28
import java .io .File ;
28
29
import java .io .IOException ;
29
30
import java .io .PrintWriter ;
31
+ import java .util .Base64 ;
30
32
import java .util .HashSet ;
31
33
import java .util .Scanner ;
32
34
import java .util .concurrent .Callable ;
33
35
import java .util .prefs .InvalidPreferencesFormatException ;
34
36
import java .util .stream .Collectors ;
35
37
36
- @ Command (name = "blobsaver" , versionProvider = CLI .VersionProvider .class , header = CLI .warning ,
38
+ @ Command (name = "blobsaver" , versionProvider = CLI .VersionProvider .class , mixinStandardHelpOptions = true ,
39
+ sortOptions = false , usageHelpAutoWidth = true , sortSynopsis = false , abbreviateSynopsis = true ,
37
40
optionListHeading = " You can separate options and their parameters with either a space or '='.%n" ,
38
- mixinStandardHelpOptions = true , sortOptions = false , usageHelpAutoWidth = true , sortSynopsis = false ,
39
- abbreviateSynopsis = true , synopsisSubcommandLabel = "" )
41
+ commandListHeading = "Commands:%n See @|bold,white blobsaver help [COMMAND]|@ for more information about each command.%n%n" ,
42
+ subcommands = HelpCommand . class )
40
43
public class CLI implements Callable <Void > {
41
44
42
- public static final String warning = "Warning: blobsaver's CLI is in alpha. Commands, options, and exit codes may change at any time.%n" ;
43
-
44
45
@ Option (names = {"-s" , "--save-blobs" })
45
46
boolean saveBlobs ;
46
47
@@ -50,10 +51,12 @@ public class CLI implements Callable<Void> {
50
51
@ Option (names = "--remove-device" , paramLabel = "<Saved Device>" , description = "Remove a saved device." )
51
52
Prefs .SavedDevice removeDevice ;
52
53
53
- @ Option (names = "--enable-background" , paramLabel = "<Saved Device>" , description = "Enable background saving for a device.%nUse '--start-background-service' once devices are added." )
54
+ @ Option (names = "--enable-background" , paramLabel = "<Saved Device>" ,
55
+ description = "Enable background saving for a device.%nUse '--start-background-service' once devices are added." )
54
56
Prefs .SavedDevice enableBackground ;
55
57
56
- @ Option (names = "--disable-background" , paramLabel = "<Saved Device>" , description = "Disable background saving for a device." )
58
+ @ Option (names = "--disable-background" , paramLabel = "<Saved Device>" ,
59
+ description = "Disable background saving for a device." )
57
60
Prefs .SavedDevice disableBackground ;
58
61
59
62
@ ArgGroup
@@ -68,7 +71,7 @@ static class BackgroundControls {
68
71
boolean backgroundAutosave ;
69
72
}
70
73
71
- @ Option (names = "--export" , paramLabel = "<path>" , description = "Export saved devices in XML format to the directory." )
74
+ @ Option (names = "--export" , paramLabel = "<path>" , description = "Export saved devices in XML format to a directory." )
72
75
File exportPath ;
73
76
74
77
@ Option (names = "--import" , paramLabel = "<path>" , description = "Import saved devices from a blobsaver XML file." )
@@ -94,7 +97,7 @@ static class BackgroundControls {
94
97
File savePath ;
95
98
96
99
@ ArgGroup
97
- Version version ;
100
+ Version version = new Version () ;
98
101
static class Version {
99
102
@ Option (names = "--ios-version" , paramLabel = "<version>" )
100
103
String manualVersion ;
@@ -111,15 +114,15 @@ static class Version {
111
114
public Void call () throws TSS .TSSException , IOException , InvalidPreferencesFormatException {
112
115
if (importPath != null ) {
113
116
Prefs .importXML (importPath );
114
- System .out .println ("Successfully imported saved devices." );
117
+ System .out .println (success ( "Successfully imported saved devices." ) );
115
118
}
116
119
if (saveBlobs ) {
117
120
checkArgs ("identifier" , "ecid" , "save-path" );
118
121
var tss = new TSS .Builder ()
119
122
.setDevice (device ).setEcid (ecid ).setSavePath (savePath .getCanonicalPath ()).setBoardConfig (boardConfig )
120
123
.setManualVersion (version .manualVersion ).setManualIpswURL (version .manualIpswURL ).setApnonce (apnonce )
121
124
.setGenerator (generator ).setIncludeBetas (version .includeBetas ).build ();
122
- System .out .println (tss .call ());
125
+ System .out .println (success ( " \n " + tss .call () ));
123
126
}
124
127
if (removeDevice != null ) {
125
128
removeDevice .delete ();
@@ -130,15 +133,15 @@ public Void call() throws TSS.TSSException, IOException, InvalidPreferencesForma
130
133
var saved = new Prefs .SavedDeviceBuilder (saveDevice )
131
134
.setIdentifier (device ).setEcid (ecid ).setSavePath (savePath .getCanonicalPath ()).setBoardConfig (boardConfig )
132
135
.setApnonce (apnonce ).setGenerator (generator ).setIncludeBetas (version .includeBetas ).save ();
133
- System .out .println ("Saved " + saved + "." );
136
+ System .out .println (success ( "Saved " + saved + "." ) );
134
137
}
135
138
if (enableBackground != null ) {
136
139
if (!saveBlobs ) {
137
140
System .out .println ("Testing device\n " );
138
141
Background .saveBlobs (enableBackground );
139
142
}
140
143
enableBackground .setBackground (true );
141
- System .out .println ("Enabled background for " + enableBackground + "." );
144
+ System .out .println (success ( " \n Enabled background for " + enableBackground + "." ) );
142
145
}
143
146
if (disableBackground != null ) {
144
147
disableBackground .setBackground (false );
@@ -147,7 +150,7 @@ public Void call() throws TSS.TSSException, IOException, InvalidPreferencesForma
147
150
if (backgroundControls .startBackground ) {
148
151
Background .startBackground ();
149
152
if (Background .isBackgroundEnabled ()) {
150
- System .out .println ("A background saving task has been scheduled." );
153
+ System .out .println (success ( "A background saving task has been scheduled." ) );
151
154
} else {
152
155
throw new ExecutionException (spec .commandLine (), "Error: Unable to enable background saving." );
153
156
}
@@ -162,12 +165,11 @@ public Void call() throws TSS.TSSException, IOException, InvalidPreferencesForma
162
165
exportPath = new File (exportPath , "blobsaver.xml" );
163
166
}
164
167
Prefs .export (exportPath );
165
- System .out .println ("Successfully exported saved devices." );
168
+ System .out .println (success ( "Successfully exported saved devices." ) );
166
169
}
167
170
return null ;
168
171
}
169
172
170
- @ SuppressWarnings ("unused" )
171
173
@ Command (name = "clear-app-data" , description = "Remove all of blobsaver's data including saved devices." )
172
174
void clearAppData () {
173
175
System .out .print ("Are you sure you would like to permanently clear all blobsaver data? " );
@@ -178,31 +180,87 @@ void clearAppData() {
178
180
}
179
181
}
180
182
181
- @ SuppressWarnings ("unused" )
182
- @ Command (name = "donate" , description = "https://www.paypal.me/airsqrd" )
183
+ @ Command (description = "Help support me and the development of this application! (I'm only a student)" )
183
184
void donate () {
184
- System .out .println ("You can donate at https://www.paypal.me/airsqrd or with GitHub Sponsors at https://github.com/sponsors/airsquared." );
185
+ System .out .println ("""
186
+ You can donate with GitHub Sponsors at
187
+ https://github.com/sponsors/airsquared
188
+ or with PayPal at
189
+ https://www.paypal.me/airsqrd.
190
+ Thank you!""" );
191
+ }
192
+
193
+ @ Command (name = "read-info" , description = "Reads ECID, identifier, board configuration, device type, and device name." )
194
+ void readInfo () throws LibimobiledeviceException {
195
+ long ecid = LibimobiledeviceUtil .getECID ();
196
+ System .out .println ("ECID (hex): " + Long .toHexString (ecid ).toUpperCase ());
197
+ System .out .println ("ECID (dec): " + ecid );
198
+ String identifier = LibimobiledeviceUtil .getDeviceModelIdentifier ();
199
+ System .out .println ("Identifier: " + identifier );
200
+ System .out .println ("Board Configuration: " + LibimobiledeviceUtil .getBoardConfig ()
201
+ + (Devices .doesRequireBoardConfig (identifier ) ? " (Required)" : " (Not Required)" ));
202
+ System .out .println ("Device Type: " + Devices .getDeviceType (identifier ));
203
+ System .out .println ("Device Name: " + Devices .identifierToModel (identifier ));
204
+
205
+ Analytics .readInfo ();
206
+ }
207
+
208
+ @ Command (name = "read-apnonce" , description = "Enters recovery mode to read APNonce, and freezes it if needed." )
209
+ void readAPNonce (@ Option (names = "--force-new" , description = "Generate a new APNonce, even if it is already frozen." ) boolean forceNew ) throws LibimobiledeviceException {
210
+ var task = new LibimobiledeviceUtil .GetApnonceTask (forceNew ) {
211
+ @ Override
212
+ protected void updateMessage (String message ) {
213
+ System .out .println (message );
214
+ }
215
+ };
216
+ task .call ();
217
+ System .out .println ("APNonce: " + task .getApnonceResult ());
218
+ System .out .println ("Generator: " + task .getGeneratorResult ());
219
+ }
220
+
221
+ @ Command (name = "exit-recovery" )
222
+ void exitRecovery () throws LibimobiledeviceException {
223
+ LibimobiledeviceUtil .exitRecovery ();
224
+ Analytics .exitRecovery ();
225
+ }
226
+
227
+ @ Command (description = "Read a key from lockdownd." , showDefaultValues = true )
228
+ void read (@ Parameters (paramLabel = "<key>" ) String key ,
229
+ @ Parameters (paramLabel = "[output-type]" , defaultValue = "xml" , description = "Can be any of [xml, string, integer, base64]" ) PlistOutputType type ) throws LibimobiledeviceException {
230
+ var plist = LibimobiledeviceUtil .getLockdownValuePlist (key );
231
+ System .out .println (switch (type ) {
232
+ case XML -> LibimobiledeviceUtil .plistToXml (plist );
233
+ case STRING -> LibimobiledeviceUtil .getPlistString (plist );
234
+ case INTEGER -> LibimobiledeviceUtil .getPlistLong (plist );
235
+ case BASE64 -> Base64 .getEncoder ().encodeToString (LibimobiledeviceUtil .plistDataBytes (plist ));
236
+ });
185
237
}
238
+ enum PlistOutputType {XML , STRING , INTEGER , BASE64 }
186
239
187
240
public static class VersionProvider implements IVersionProvider {
188
241
@ Override
189
242
public String [] getVersion () {
190
- String [] output = {CLI .warning , "blobsaver " + Main .appVersion , Main .copyright , null };
243
+ String [] output = {"blobsaver " + Main .appVersion , Main .copyright , "Licence: GNU GPL v3.0-only" , null ,
244
+ "%nHomepage: https://github.com/airsquared/blobsaver" };
191
245
try {
192
246
var newVersion = Utils .LatestVersion .request ();
193
247
if (Main .appVersion .equals (newVersion .toString ())) {
194
- output [3 ] = "You are on the latest version." ;
248
+ output [3 ] = "%nYou are on the latest version." ;
195
249
} else {
196
- output [3 ] = "New Update Available: " + newVersion + ". Update at%n https://github.com/airsquared/blobsaver/releases" ;
250
+ output [3 ] = "%nNew Update Available: " + newVersion + ". Update using your package manager or at%n https://github.com/airsquared/blobsaver/releases" ;
197
251
}
198
252
} catch (Exception e ) {
199
- output [3 ] = "Unable to check for updates." ;
253
+ output [3 ] = "%nUnable to check for updates." ;
200
254
}
201
255
202
256
return output ;
203
257
}
204
258
}
205
259
260
+ private static String success (String s ) {
261
+ return Help .Ansi .AUTO .string ("@|bold,green " + s + "|@" );
262
+ }
263
+
206
264
private void checkArgs (String ... names ) {
207
265
var missing = new HashSet <ArgSpec >();
208
266
for (String name : names ) {
@@ -223,10 +281,11 @@ private static Prefs.SavedDevice savedDeviceConverter(String name) {
223
281
.findAny ().orElseThrow (() -> new TypeConversionException ("Must be one of " + Prefs .getSavedDevices () + "\n " ));
224
282
}
225
283
226
- public static int handleExecutionException (Exception ex , CommandLine cmd , ParseResult parseResult ) throws Exception {
284
+ private static int handleExecutionException (Exception ex , CommandLine cmd , ParseResult parseResult ) throws Exception {
227
285
boolean messageOnly = ex instanceof ExecutionException
228
286
// if either the exception is not reportable or there is a tssLog present
229
- || ex instanceof TSS .TSSException e && (!e .isReportable || e .tssLog != null );
287
+ || ex instanceof TSS .TSSException e && (!e .isReportable || e .tssLog != null )
288
+ || ex instanceof LibimobiledeviceException ;
230
289
if (messageOnly ) {
231
290
cmd .getErr ().println (cmd .getColorScheme ().errorText (ex .getMessage ()));
232
291
@@ -237,8 +296,10 @@ public static int handleExecutionException(Exception ex, CommandLine cmd, ParseR
237
296
throw ex ;
238
297
}
239
298
240
- public static int handleParseException (ParameterException ex , String [] args ) {
299
+ private static int handleParameterException (ParameterException ex , String [] args ) {
241
300
CommandLine cmd = ex .getCommandLine ();
301
+ CommandSpec spec = cmd .getCommandSpec ();
302
+ boolean isRootCommand = spec == spec .root ();
242
303
PrintWriter err = cmd .getErr ();
243
304
244
305
// if tracing at DEBUG level, show the location of the issue
@@ -248,10 +309,12 @@ public static int handleParseException(ParameterException ex, String[] args) {
248
309
249
310
err .println (cmd .getColorScheme ().errorText (ex .getMessage ())); // bold red
250
311
UnmatchedArgumentException .printSuggestions (ex , err );
251
- err .print (cmd .getHelp ().fullSynopsis ());
252
-
253
- CommandSpec spec = cmd .getCommandSpec ();
254
- err .printf ("Try '%s --help' for more information.%n" , spec .qualifiedName ());
312
+ if (isRootCommand ) {
313
+ err .print (cmd .getHelp ().fullSynopsis ());
314
+ err .printf ("Try '%s help' for more information.%n" , spec .name ());
315
+ } else {
316
+ cmd .usage (err ); // print full help
317
+ }
255
318
256
319
return cmd .getExitCodeExceptionMapper () != null
257
320
? cmd .getExitCodeExceptionMapper ().getExitCode (ex )
@@ -262,10 +325,20 @@ public static int handleParseException(ParameterException ex, String[] args) {
262
325
* @return the exit code
263
326
*/
264
327
public static int launch (String ... args ) {
328
+ Analytics .startup ();
265
329
var c = new CommandLine (new CLI ())
330
+ .setCaseInsensitiveEnumValuesAllowed (true )
266
331
.setExecutionExceptionHandler (CLI ::handleExecutionException )
267
- .setParameterExceptionHandler (CLI ::handleParseException )
332
+ .setParameterExceptionHandler (CLI ::handleParameterException )
268
333
.registerConverter (Prefs .SavedDevice .class , CLI ::savedDeviceConverter );
334
+ if (args .length == 0 ) { // happens when environment variable $BLOBSAVER_CLI_ONLY is set to some value
335
+ args = new String []{"help" };
336
+ }
269
337
return c .execute (args );
270
338
}
339
+
340
+ /**
341
+ * Private Constructor; Use {@link CLI#launch(String...)} instead
342
+ */
343
+ private CLI () {}
271
344
}
0 commit comments