forked from apple/containerization
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEXT4+Xattrs.swift
More file actions
317 lines (289 loc) · 12.4 KB
/
Copy pathEXT4+Xattrs.swift
File metadata and controls
317 lines (289 loc) · 12.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
//===----------------------------------------------------------------------===//
// Copyright © 2025-2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import Foundation
/*
* Note: Both the entries and values for the attributes need to occupy a size that is a multiple of 4,
* meaning, in cases where the attribute name or value is not a multiple of 4, it is padded with 0
* until it reaches that size.
*/
extension EXT4 {
public struct ExtendedAttribute {
public static let prefixMap: [Int: String] = [
1: "user.",
2: "system.posix_acl_access",
3: "system.posix_acl_default",
4: "trusted.",
6: "security.",
7: "system.",
8: "system.richacl",
]
let name: String
let index: UInt8
let value: [UInt8]
var sizeValue: UInt32 {
UInt32((value.count + 3) & ~3)
}
var sizeEntry: UInt32 {
UInt32((name.count + 3) & ~3 + 16) // 16 bytes are needed to store other metadata for the xattr entry
}
var size: UInt32 {
sizeEntry + sizeValue
}
var fullName: String {
Self.decompressName(id: Int(index), suffix: name)
}
var hash: UInt32 {
var hash: UInt32 = 0
for char in name {
hash = (hash << 5) ^ (hash >> 27) ^ UInt32(char.asciiValue!)
}
var i = 0
while i + 3 < value.count {
let s = value[i..<i + 4]
let v = s.withUnsafeBytes { $0.loadLittleEndian(as: UInt32.self) }
hash = (hash << 16) ^ (hash >> 16) ^ v
i += 4
}
if value.count % 4 != 0 {
let last = value.count & ~3
var buff: [UInt8] = [0, 0, 0, 0]
for (i, byte) in value[last...].enumerated() {
buff[i] = byte
}
let v = buff.withUnsafeBytes { $0.loadLittleEndian(as: UInt32.self) }
hash = (hash << 16) ^ (hash >> 16) ^ v
}
return hash
}
init(name: String, value: [UInt8]) {
let compressed = Self.compressName(name)
self.name = compressed.str
self.index = compressed.id
self.value = value
}
init(idx: UInt8, compressedName name: String, value: [UInt8]) {
self.name = name
self.index = idx
self.value = value
}
// MARK: Class methods
public static func compressName(_ name: String) -> (id: UInt8, str: String) {
for (id, prefix) in prefixMap.sorted(by: { $1.1.count < $0.1.count }) where name.hasPrefix(prefix) {
return (UInt8(id), String(name.dropFirst(prefix.count)))
}
return (0, name)
}
public static func decompressName(id: Int, suffix: String) -> String {
guard let prefix = prefixMap[id] else {
return suffix
}
return "\(prefix)\(suffix)"
}
}
public struct FileXattrsState {
private let inodeCapacity: UInt32
private let blockCapacity: UInt32
private let inode: UInt32 // the inode number for which we are tracking these xattrs
var inlineAttributes: [ExtendedAttribute] = []
var blockAttributes: [ExtendedAttribute] = []
private var usedSizeInline: UInt32 = 0
private var usedSizeBlock: UInt32 = 0
private var inodeFreeBytes: UInt32 {
self.inodeCapacity - EXT4.XattrInodeHeaderSize - usedSizeInline - 4 // need to have 4 null bytes b/w xattr entries and values
}
private var blockFreeBytes: UInt32 {
self.blockCapacity - EXT4.XattrBlockHeaderSize - usedSizeBlock - 4
}
init(inode: UInt32, inodeXattrCapacity: UInt32, blockCapacity: UInt32) {
self.inode = inode
self.inodeCapacity = inodeXattrCapacity
self.blockCapacity = blockCapacity
}
public mutating func add(_ attribute: ExtendedAttribute) throws {
let size = attribute.size
if size <= inodeFreeBytes {
usedSizeInline += size
inlineAttributes.append(attribute)
return
}
if size <= blockFreeBytes {
usedSizeBlock += size
blockAttributes.append(attribute)
return
}
throw Error.insufficientSpace(Int(self.inode))
}
public func writeInlineAttributes(buffer: inout [UInt8]) throws {
var idx = 0
withUnsafeLittleEndianBytes(
of: EXT4.XAttrHeaderMagic,
body: { bytes in
for byte in bytes {
buffer[idx] = byte
idx += 1
}
})
try Self.write(buffer: &buffer, attrs: self.inlineAttributes, start: UInt16(idx), delta: 0, inline: true)
}
public func writeBlockAttributes(buffer: inout [UInt8]) throws {
var idx = 0
for val in [EXT4.XAttrHeaderMagic, 1, 1] {
withUnsafeLittleEndianBytes(
of: UInt32(val),
body: { bytes in
for byte in bytes {
buffer[idx] = byte
idx += 1
}
})
}
while idx != 32 {
buffer[idx] = 0
idx += 1
}
var attributes = self.blockAttributes
attributes.sort(by: {
if $0.index != $1.index {
return $0.index < $1.index
}
if $0.name.count != $1.name.count {
return $0.name.count < $1.name.count
}
return $0.name < $1.name
})
try Self.write(buffer: &buffer, attrs: attributes, start: UInt16(idx), delta: UInt16(idx), inline: false)
}
/// Writes the specified list of extended attribute entries and their values to the provided
/// This method does not fill in any headers (Inode inline / block level) that may be required to parse these attributes
///
/// - Parameters:
/// - buffer: An array of [UInt8] where the data will be written into
/// - attrs: The list of ExtendedAttributes to write
/// - start: the index from where data should be put into the buffer - useful when if you dont want this method to be overwriting existing data
/// - delta: index from where the begin the offset calculations
/// - inline: if the byte buffer being written into is an inline data block for an inode: Determines the hash calculation
private static func write(
buffer: inout [UInt8], attrs: [ExtendedAttribute], start: UInt16, delta: UInt16, inline: Bool
) throws {
var offset: UInt16 = UInt16(buffer.count) + delta - start
var front = Int(start)
var end = buffer.count
for attribute in attrs {
guard end - front >= 4 else {
throw Error.malformedXattrBuffer
}
var out: [UInt8] = []
let v = attribute.sizeValue
offset -= UInt16(v)
out.append(UInt8(attribute.name.count))
out.append(attribute.index)
withUnsafeLittleEndianBytes(
of: UInt16(offset),
body: { bytes in
out.append(contentsOf: bytes)
})
out.append(contentsOf: [0, 0, 0, 0]) // these next four bytes indicate that the attr values are in the same block
withUnsafeLittleEndianBytes(
of: UInt32(attribute.value.count),
body: { bytes in
out.append(contentsOf: bytes)
})
if !inline {
withUnsafeLittleEndianBytes(
of: UInt32(attribute.hash),
body: { bytes in
out.append(contentsOf: bytes)
})
} else {
out.append(contentsOf: [0, 0, 0, 0])
}
guard let name = attribute.name.data(using: .ascii) else {
throw Error.convertAsciiString(attribute.name)
}
out.append(contentsOf: [UInt8](name))
while out.count < Int(attribute.sizeEntry) { // ensure that xattr entry size is a multiple of 4
out.append(0)
}
for (i, byte) in out.enumerated() {
buffer[front + i] = byte
}
front += out.count
end -= Int(attribute.sizeValue)
for (i, byte) in attribute.value.enumerated() {
buffer[end + i] = byte
}
}
}
public static func read(buffer: [UInt8], start: Int, offset: Int) throws -> [ExtendedAttribute] {
var i = start
var attribs: [ExtendedAttribute] = []
// 16 is the size of 1 XAttrEntry
while i + 16 <= buffer.count {
let attributeStart = i
let rawXattrEntry = Array(buffer[i..<i + 16])
let xattrEntry = try EXT4.XAttrEntry(using: rawXattrEntry)
i += 16
var endIndex = i + Int(xattrEntry.nameLength)
guard endIndex <= buffer.count else {
continue
}
let rawName = buffer[i..<endIndex]
guard let name = String(bytes: rawName, encoding: .ascii) else {
throw Error.nonAsciiXattrName
}
let valueStart = Int(xattrEntry.valueOffset) + offset
let valueEnd = Int(xattrEntry.valueOffset) + Int(xattrEntry.valueSize) + offset
guard valueEnd <= buffer.count else {
break
}
let value = [UInt8](buffer[valueStart..<valueEnd])
let xattr = ExtendedAttribute(idx: xattrEntry.nameIndex, compressedName: name, value: value)
attribs.append(xattr)
i = attributeStart + xattr.sizeEntry
// The next 4 bytes being null indicate that there are no more attributes to read
endIndex = i + 3
guard endIndex < buffer.count else {
continue
}
if Array(buffer[i...i + 3]) == [0, 0, 0, 0] {
break
}
}
return attribs
}
public enum Error: CustomStringConvertible, Swift.Error {
case insufficientSpace(_ inode: Int)
case malformedXattrBuffer
case convertAsciiString(_ s: String)
case nonAsciiXattrName
case missingXAttrHeader
public var description: String {
switch self {
case .insufficientSpace(let inode):
return "cannot fit xattr for inode \(inode)"
case .malformedXattrBuffer:
return "malformed extended attribute buffer"
case .convertAsciiString(let s):
return "cannot convert string \(s) to a list of ASCII characters"
case .nonAsciiXattrName:
return "extended attribute name contains non-ASCII bytes"
case .missingXAttrHeader:
return "missing header for extended attribute entry"
}
}
}
}
}