@@ -13,10 +13,162 @@ struct Plist2Profile: ParsableCommand {
13
13
static var configuration = CommandConfiguration (
14
14
commandName: " plist2profile " ,
15
15
abstract: " converts a standard preference plist file to a mobileconfig profile " ,
16
+ usage: " plist2profile <identifier> <plist> ... " ,
16
17
version: " 0.1 "
17
18
)
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 ( )
18
142
19
- func run( ) {
20
143
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> " )
21
173
}
22
174
}
0 commit comments