1818
1919package airsquared .blobsaver .app ;
2020
21+ import airsquared .blobsaver .app .LibimobiledeviceUtil .LibimobiledeviceException ;
2122import picocli .CommandLine ;
2223import picocli .CommandLine .*;
2324import picocli .CommandLine .Model .ArgSpec ;
2728import java .io .File ;
2829import java .io .IOException ;
2930import java .io .PrintWriter ;
31+ import java .util .Base64 ;
3032import java .util .HashSet ;
3133import java .util .Scanner ;
3234import java .util .concurrent .Callable ;
3335import java .util .prefs .InvalidPreferencesFormatException ;
3436import java .util .stream .Collectors ;
3537
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 ,
3740 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 )
4043public class CLI implements Callable <Void > {
4144
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-
4445 @ Option (names = {"-s" , "--save-blobs" })
4546 boolean saveBlobs ;
4647
@@ -50,10 +51,12 @@ public class CLI implements Callable<Void> {
5051 @ Option (names = "--remove-device" , paramLabel = "<Saved Device>" , description = "Remove a saved device." )
5152 Prefs .SavedDevice removeDevice ;
5253
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." )
5456 Prefs .SavedDevice enableBackground ;
5557
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." )
5760 Prefs .SavedDevice disableBackground ;
5861
5962 @ ArgGroup
@@ -68,7 +71,7 @@ static class BackgroundControls {
6871 boolean backgroundAutosave ;
6972 }
7073
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." )
7275 File exportPath ;
7376
7477 @ Option (names = "--import" , paramLabel = "<path>" , description = "Import saved devices from a blobsaver XML file." )
@@ -94,7 +97,7 @@ static class BackgroundControls {
9497 File savePath ;
9598
9699 @ ArgGroup
97- Version version ;
100+ Version version = new Version () ;
98101 static class Version {
99102 @ Option (names = "--ios-version" , paramLabel = "<version>" )
100103 String manualVersion ;
@@ -111,15 +114,15 @@ static class Version {
111114 public Void call () throws TSS .TSSException , IOException , InvalidPreferencesFormatException {
112115 if (importPath != null ) {
113116 Prefs .importXML (importPath );
114- System .out .println ("Successfully imported saved devices." );
117+ System .out .println (success ( "Successfully imported saved devices." ) );
115118 }
116119 if (saveBlobs ) {
117120 checkArgs ("identifier" , "ecid" , "save-path" );
118121 var tss = new TSS .Builder ()
119122 .setDevice (device ).setEcid (ecid ).setSavePath (savePath .getCanonicalPath ()).setBoardConfig (boardConfig )
120123 .setManualVersion (version .manualVersion ).setManualIpswURL (version .manualIpswURL ).setApnonce (apnonce )
121124 .setGenerator (generator ).setIncludeBetas (version .includeBetas ).build ();
122- System .out .println (tss .call ());
125+ System .out .println (success ( " \n " + tss .call () ));
123126 }
124127 if (removeDevice != null ) {
125128 removeDevice .delete ();
@@ -130,15 +133,15 @@ public Void call() throws TSS.TSSException, IOException, InvalidPreferencesForma
130133 var saved = new Prefs .SavedDeviceBuilder (saveDevice )
131134 .setIdentifier (device ).setEcid (ecid ).setSavePath (savePath .getCanonicalPath ()).setBoardConfig (boardConfig )
132135 .setApnonce (apnonce ).setGenerator (generator ).setIncludeBetas (version .includeBetas ).save ();
133- System .out .println ("Saved " + saved + "." );
136+ System .out .println (success ( "Saved " + saved + "." ) );
134137 }
135138 if (enableBackground != null ) {
136139 if (!saveBlobs ) {
137140 System .out .println ("Testing device\n " );
138141 Background .saveBlobs (enableBackground );
139142 }
140143 enableBackground .setBackground (true );
141- System .out .println ("Enabled background for " + enableBackground + "." );
144+ System .out .println (success ( " \n Enabled background for " + enableBackground + "." ) );
142145 }
143146 if (disableBackground != null ) {
144147 disableBackground .setBackground (false );
@@ -147,7 +150,7 @@ public Void call() throws TSS.TSSException, IOException, InvalidPreferencesForma
147150 if (backgroundControls .startBackground ) {
148151 Background .startBackground ();
149152 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." ) );
151154 } else {
152155 throw new ExecutionException (spec .commandLine (), "Error: Unable to enable background saving." );
153156 }
@@ -162,12 +165,11 @@ public Void call() throws TSS.TSSException, IOException, InvalidPreferencesForma
162165 exportPath = new File (exportPath , "blobsaver.xml" );
163166 }
164167 Prefs .export (exportPath );
165- System .out .println ("Successfully exported saved devices." );
168+ System .out .println (success ( "Successfully exported saved devices." ) );
166169 }
167170 return null ;
168171 }
169172
170- @ SuppressWarnings ("unused" )
171173 @ Command (name = "clear-app-data" , description = "Remove all of blobsaver's data including saved devices." )
172174 void clearAppData () {
173175 System .out .print ("Are you sure you would like to permanently clear all blobsaver data? " );
@@ -178,31 +180,87 @@ void clearAppData() {
178180 }
179181 }
180182
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)" )
183184 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+ });
185237 }
238+ enum PlistOutputType {XML , STRING , INTEGER , BASE64 }
186239
187240 public static class VersionProvider implements IVersionProvider {
188241 @ Override
189242 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" };
191245 try {
192246 var newVersion = Utils .LatestVersion .request ();
193247 if (Main .appVersion .equals (newVersion .toString ())) {
194- output [3 ] = "You are on the latest version." ;
248+ output [3 ] = "%nYou are on the latest version." ;
195249 } 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" ;
197251 }
198252 } catch (Exception e ) {
199- output [3 ] = "Unable to check for updates." ;
253+ output [3 ] = "%nUnable to check for updates." ;
200254 }
201255
202256 return output ;
203257 }
204258 }
205259
260+ private static String success (String s ) {
261+ return Help .Ansi .AUTO .string ("@|bold,green " + s + "|@" );
262+ }
263+
206264 private void checkArgs (String ... names ) {
207265 var missing = new HashSet <ArgSpec >();
208266 for (String name : names ) {
@@ -223,10 +281,11 @@ private static Prefs.SavedDevice savedDeviceConverter(String name) {
223281 .findAny ().orElseThrow (() -> new TypeConversionException ("Must be one of " + Prefs .getSavedDevices () + "\n " ));
224282 }
225283
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 {
227285 boolean messageOnly = ex instanceof ExecutionException
228286 // 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 ;
230289 if (messageOnly ) {
231290 cmd .getErr ().println (cmd .getColorScheme ().errorText (ex .getMessage ()));
232291
@@ -237,8 +296,10 @@ public static int handleExecutionException(Exception ex, CommandLine cmd, ParseR
237296 throw ex ;
238297 }
239298
240- public static int handleParseException (ParameterException ex , String [] args ) {
299+ private static int handleParameterException (ParameterException ex , String [] args ) {
241300 CommandLine cmd = ex .getCommandLine ();
301+ CommandSpec spec = cmd .getCommandSpec ();
302+ boolean isRootCommand = spec == spec .root ();
242303 PrintWriter err = cmd .getErr ();
243304
244305 // if tracing at DEBUG level, show the location of the issue
@@ -248,10 +309,12 @@ public static int handleParseException(ParameterException ex, String[] args) {
248309
249310 err .println (cmd .getColorScheme ().errorText (ex .getMessage ())); // bold red
250311 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+ }
255318
256319 return cmd .getExitCodeExceptionMapper () != null
257320 ? cmd .getExitCodeExceptionMapper ().getExitCode (ex )
@@ -262,10 +325,20 @@ public static int handleParseException(ParameterException ex, String[] args) {
262325 * @return the exit code
263326 */
264327 public static int launch (String ... args ) {
328+ Analytics .startup ();
265329 var c = new CommandLine (new CLI ())
330+ .setCaseInsensitiveEnumValuesAllowed (true )
266331 .setExecutionExceptionHandler (CLI ::handleExecutionException )
267- .setParameterExceptionHandler (CLI ::handleParseException )
332+ .setParameterExceptionHandler (CLI ::handleParameterException )
268333 .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+ }
269337 return c .execute (args );
270338 }
339+
340+ /**
341+ * Private Constructor; Use {@link CLI#launch(String...)} instead
342+ */
343+ private CLI () {}
271344}
0 commit comments