@@ -13,10 +13,162 @@ struct Plist2Profile: ParsableCommand {
1313 static var configuration = CommandConfiguration (
1414 commandName: " plist2profile " ,
1515 abstract: " converts a standard preference plist file to a mobileconfig profile " ,
16+ usage: " plist2profile <identifier> <plist> ... " ,
1617 version: " 0.1 "
1718 )
19+
20+ // MARK: arguments,options, flags
21+ @Argument (
22+ help: ArgumentHelp (
23+ " the payload identifier for the profile " ,
24+ valueName: " identifier "
25+ )
26+ )
27+ var identifier : String
28+
29+ @Argument (
30+ help: ArgumentHelp (
31+ " Path to a plist to be added as a profile payload. Can be specified multiple times. " ,
32+ valueName: " plist "
33+ )
34+ )
35+ var plistPaths : [ String ]
36+
37+ @Option (
38+ name: [ . customShort( " g " ) , . customLong( " organization " ) ] ,
39+ help: " Cosmetic name for the organization deploying the profile. "
40+ )
41+ var organization = " "
42+
43+ @Option (
44+ name: [ . customShort( " o " ) , . customLong( " output " ) ] ,
45+ help: " Output path for profile. Defaults to 'identifier.mobileconfig' in the current working directory. "
46+ )
47+ var outputPath = " "
48+
49+ @Option (
50+ name: [ . customShort( " d " ) , . customLong( " displayname " ) ] ,
51+ help: " Display name for profile. Defaults to 'plist2profile: <first domain>'. "
52+ )
53+ var displayName = " "
54+
55+ // TODO: option to create a modern or mcx profile
56+
57+ // MARK: variables
58+
59+ var uuid = UUID ( )
60+ var payloadVersion = 1
61+ var payloadType = " Configuration "
62+ var payloadScope = " System " // or "User"
63+
64+ // TODO: missing keys for profile
65+ // payload scope
66+ // removal disallowed
67+ // removalDate, duration until removal
68+ // description
69+ //
70+ // all of these should at least be grabbed when initialising from a file
71+ //
72+
73+ // MARK: functions
74+
75+ // TODO: can we put these functions in shared file? Can we share files between targets in a package without creating a library?
76+
77+ func exit( _ message: Any , code: Int32 = 1 ) throws -> Never {
78+ print ( message)
79+ throw ExitCode ( code)
80+ }
81+
82+ func isReadableFilePath( _ path: String ) throws {
83+ let fm = FileManager . default
84+ var isDirectory : ObjCBool = false
85+ if !fm. fileExists ( atPath: path, isDirectory: & isDirectory) {
86+ try exit ( " no file at path ' \( path) '! " , code: 66 )
87+ }
88+ if isDirectory. boolValue {
89+ try exit ( " path ' \( path) ' is a directory " , code: 66 )
90+ }
91+ if !fm. isReadableFile ( atPath: path) {
92+ try exit ( " cannot read file at ' \( path) '! " , code: 66 )
93+ }
94+ }
95+
96+ mutating func populateDefaults( ) {
97+ // if displayName is empty, populate
98+ if displayName. isEmpty {
99+ displayName = " plist2Profile: \( identifier) "
100+ }
101+
102+ // if output is empty, generate file name
103+ if outputPath. isEmpty {
104+ outputPath = identifier. appending ( " .plist " )
105+ }
106+ }
107+
108+ func validatePlists( ) throws {
109+ for plistPath in plistPaths {
110+ try isReadableFilePath ( plistPath)
111+ }
112+ }
113+
114+ func createModernPayload( plistPath: String ) throws -> NSDictionary {
115+ let payloadUUID = UUID ( )
116+ // determine filename from path
117+ let plistURL = URL ( fileURLWithPath: plistPath)
118+ let plistname = plistURL. deletingPathExtension ( ) . lastPathComponent
119+ guard let payload = try ? NSMutableDictionary ( contentsOf: plistURL, error: ( ) )
120+ else {
121+ try exit ( " file at ' \( plistPath) ' might not be a plist! " , code: 65 )
122+ }
123+ // payload keys
124+ payload [ " PayloadIdentifier " ] = plistname
125+ payload [ " PayloadType " ] = plistname
126+ payload [ " PayloadDisplayName " ] = " \( displayName) : \( plistname) "
127+ payload [ " PayloadUUID " ] = payloadUUID. description
128+ payload [ " PayloadVersion " ] = payloadVersion
129+
130+ if !organization. isEmpty {
131+ payload [ " PayloadOrganization " ] = organization
132+ }
133+ return payload
134+ }
135+
136+ // MARK: run
137+
138+ mutating func run( ) throws {
139+ // TODO: if identifer points to a mobile config file, get data from there
140+ try validatePlists ( )
141+ populateDefaults ( )
18142
19- func run( ) {
20143 print ( " Hello, plist2profile! " )
144+
145+ // Boilerplate
146+ let profileDict : NSMutableDictionary = [
147+ " PayloadIdentifier " : identifier,
148+ " PayloadUUID " : uuid. description,
149+ " PayloadVersion " : payloadVersion,
150+ " PayloadType " : payloadType,
151+ " PayloadDisplayName " : displayName,
152+ " PayloadScope " : payloadScope
153+ ]
154+
155+ if !organization. isEmpty {
156+ profileDict [ " PayloadOrganization " ] = organization
157+ }
158+
159+ let payloads = NSMutableArray ( )
160+
161+ for plistPath in plistPaths {
162+ let payload = try createModernPayload ( plistPath: plistPath)
163+ payloads. add ( payload)
164+
165+ // insert payloads array
166+ profileDict [ " PayloadContent " ] = payloads
167+ }
168+
169+ guard let plistData = try ? PropertyListSerialization . data ( fromPropertyList: profileDict, format: . xml, options: . zero)
170+ else { try exit ( " could generate property list " , code: 73 ) }
171+
172+ print ( String ( data: plistData, encoding: . utf8) ?? " <no data> " )
21173 }
22174}
0 commit comments