| // RUN: %empty-directory(%t) |
| // RUN: %target-build-swift %s -swift-version 5 -DPTR_SIZE_%target-ptrsize -o %t/OSLogExecutionTest |
| // RUN: %target-codesign %t/OSLogExecutionTest |
| // RUN: %target-run %t/OSLogExecutionTest |
| // |
| // RUN: %target-build-swift %s -O -swift-version 5 -DPTR_SIZE_%target-ptrsize -o %t/OSLogExecutionTest |
| // RUN: %target-codesign %t/OSLogExecutionTest |
| // RUN: %target-run %t/OSLogExecutionTest |
| // REQUIRES: executable_test |
| // REQUIRES: foundation |
| // |
| // REQUIRES: VENDOR=apple |
| |
| // Run-time tests for testing the correctness of the optimizations that optimize the |
| // construction of the format string and the byte buffer from a string interpolation. |
| // The tests here are run with -Onone (which includes only mandatory optimizations) |
| // and also with full optimizations -O. |
| |
| import OSLogTestHelper |
| import StdlibUnittest |
| import Foundation |
| |
| defer { runAllTests() } |
| |
| internal var InterpolationTestSuite = TestSuite("OSLogInterpolationTest") |
| internal let bitsPerByte = 8 |
| |
| /// A struct that provides methods for checking whether a given byte buffer |
| /// conforms to the format expected by the os_log ABI. This struct acts as |
| /// a specification of the byte buffer format. |
| internal struct OSLogBufferChecker { |
| |
| internal let buffer: UnsafeBufferPointer<UInt8> |
| |
| internal init(_ byteBuffer: UnsafeBufferPointer<UInt8>) { |
| buffer = byteBuffer |
| } |
| |
| /// Bit mask for setting bits in the peamble. The bits denoted by the bit |
| /// mask indicate whether there is an argument that is private, and whether |
| /// there is an argument that is non-scalar: String, NSObject or Pointer. |
| internal enum PreambleBitMask: UInt8 { |
| case privateBitMask = 0x1 |
| case nonScalarBitMask = 0x2 |
| } |
| |
| /// Check the summary bytes of the byte buffer. |
| /// - Parameters: |
| /// - argumentCount: number of arguments expected to be in the buffer. |
| /// - hasPrivate: true iff there exists a private argument |
| /// - hasNonScalar: true iff there exists a non-scalar argument |
| internal func checkSummaryBytes( |
| argumentCount: UInt8, |
| hasPrivate: Bool, |
| hasNonScalar: Bool |
| ) { |
| let preamble = buffer[0] |
| let privateBit = preamble & PreambleBitMask.privateBitMask.rawValue |
| expectEqual(hasPrivate, privateBit != 0) |
| |
| let nonScalarBit = preamble & PreambleBitMask.nonScalarBitMask.rawValue |
| expectEqual(hasNonScalar, nonScalarBit != 0) |
| |
| expectEqual(argumentCount, buffer[1]) |
| } |
| |
| /// The possible values for the argument flag as defined by the os_log ABI. |
| /// This occupies four least significant bits of the first byte of the |
| /// argument header. Two least significant bits are used to indicate privacy |
| /// and the other two bits are reserved. |
| internal enum ArgumentFlag: UInt8 { |
| case autoFlag = 0 |
| case privateFlag = 0x1 |
| case publicFlag = 0x2 |
| } |
| |
| /// The possible values for the argument type as defined by the os_log ABI. |
| /// This occupies four most significant bits of the first byte of the |
| /// argument header. |
| internal enum ArgumentType: UInt8 { |
| case scalar = 0, count = 1, string = 2, pointer = 3, object = 4, mask = 7 |
| // TODO: include wide string and errno here if needed. |
| } |
| |
| /// Check the encoding of an argument headers in the byte buffer starting from |
| /// the `startIndex` and return argument bytes. |
| private func checkArgumentHeadersAndGetBytes( |
| startIndex: Int, |
| size: UInt8, |
| flag: ArgumentFlag, |
| type: ArgumentType |
| ) -> [UInt8] { |
| let argumentHeader = buffer[startIndex] |
| expectEqual((type.rawValue << 4) | flag.rawValue, argumentHeader) |
| expectEqual(size, buffer[startIndex + 1]) |
| // Argument data starts after the two header bytes. |
| let argumentBytes: [UInt8] = |
| (0..<Int(size)).reduce(into: []) { (acc, index) in |
| acc.append(buffer[startIndex + 2 + index]) |
| } |
| return argumentBytes |
| } |
| |
| /// Check whether the bytes starting from `startIndex` contain the encoding for a |
| /// numerical value. |
| internal func checkNumeric<T>( |
| startIndex: Int, |
| flag: ArgumentFlag, |
| type: ArgumentType, |
| expectedValue: T |
| ) where T : Numeric { |
| let byteSize = UInt8(MemoryLayout<T>.size) |
| let argumentBytes = |
| checkArgumentHeadersAndGetBytes( |
| startIndex: startIndex, |
| size: byteSize, |
| flag: flag, |
| type: type) |
| withUnsafeBytes(of: expectedValue) { expectedBytes in |
| for i in 0..<Int(byteSize) { |
| expectEqual( |
| expectedBytes[i], |
| argumentBytes[i], |
| "mismatch at byte number \(i) " |
| + "of the expected value \(expectedValue)") |
| } |
| } |
| } |
| |
| /// Check whether the bytes starting from `startIndex` contain the encoding for an Int. |
| internal func checkInt<T>( |
| startIndex: Int, |
| flag: ArgumentFlag, |
| expectedInt: T |
| ) where T : FixedWidthInteger { |
| checkNumeric( |
| startIndex: startIndex, |
| flag: flag, |
| type: .scalar, |
| expectedValue: expectedInt) |
| } |
| |
| /// Check whether the bytes starting from `startIndex` contain the encoding for a count. |
| internal func checkCount( |
| startIndex: Int, |
| expectedCount: Int, |
| precision: Bool |
| ) { |
| checkNumeric( |
| startIndex: startIndex, |
| flag: .autoFlag, |
| type: precision ? .count : .scalar, |
| expectedValue: CInt(expectedCount)) |
| } |
| |
| /// Check whether the bytes starting from `startIndex` contain the encoding |
| /// for a string. |
| internal func checkString( |
| startIndex: Int, |
| flag: ArgumentFlag, |
| expectedString: String |
| ) { |
| let pointerSize = UInt8(MemoryLayout<UnsafePointer<Int8>>.size) |
| let argumentBytes = |
| checkArgumentHeadersAndGetBytes( |
| startIndex: startIndex, |
| size: pointerSize, |
| flag: flag, |
| type: .string) |
| // Read the pointer to a string stored in the buffer and compare it with |
| // the expected string using `strcmp`. Note that it is important we use a |
| // C function here to compare the string as it more closely represents |
| // the C os_log functions. |
| var stringAddress: Int = 0 |
| // Copy the bytes of the address byte by byte. Note that |
| // RawPointer.load(fromByteOffset:,_) function cannot be used here as the |
| // address: `buffer + offset` is not aligned for reading an Int. |
| for i in 0..<Int(pointerSize) { |
| stringAddress |= Int(argumentBytes[i]) << (8 * i) |
| } |
| let bufferDataPointer = UnsafePointer<Int8>(bitPattern: stringAddress) |
| expectedString.withCString { |
| let compareResult = strcmp($0, bufferDataPointer) |
| expectEqual(0, compareResult, "strcmp returned \(compareResult)") |
| } |
| } |
| |
| /// Check whether the bytes starting from `startIndex` contain the encoding |
| /// for an NSObject. |
| internal func checkNSObject( |
| startIndex: Int, |
| flag: ArgumentFlag, |
| expectedObject: NSObject |
| ) { |
| let pointerSize = UInt8(MemoryLayout<UnsafePointer<Int8>>.size) |
| let argumentBytes = |
| checkArgumentHeadersAndGetBytes( |
| startIndex: startIndex, |
| size: pointerSize, |
| flag: flag, |
| type: .object) |
| // Convert data to a pointer and check if the addresses stored in the |
| // pointer and the one in the buffer match. |
| let objectAddress = |
| Unmanaged |
| .passUnretained(expectedObject) |
| .toOpaque() |
| withUnsafeBytes(of: objectAddress) { expectedBytes in |
| for i in 0..<Int(pointerSize) { |
| // Argument data starts after the two header bytes. |
| expectEqual( |
| expectedBytes[i], |
| argumentBytes[i], |
| "mismatch at byte number \(i) " |
| + "of the expected object address \(objectAddress)") |
| } |
| } |
| } |
| |
| internal func checkDouble( |
| startIndex: Int, |
| flag: ArgumentFlag, |
| expectedValue: Double |
| ) { |
| let byteSize: UInt8 = 8 |
| let argumentBytes = |
| checkArgumentHeadersAndGetBytes( |
| startIndex: startIndex, |
| size: byteSize, |
| flag: flag, |
| type: .scalar) |
| withUnsafeBytes(of: expectedValue) { expectedBytes in |
| for i in 0..<Int(byteSize) { |
| expectEqual( |
| expectedBytes[i], |
| argumentBytes[i], |
| "mismatch at byte number \(i) " |
| + "of the expected value \(expectedValue)") |
| } |
| } |
| } |
| |
| /// Check the given assertions on the arguments stored in the byte buffer. |
| /// - Parameters: |
| /// - assertions: one assertion for each argument stored in the byte buffer. |
| internal func checkArguments(_ assertions: [(Int) -> Void]) { |
| var currentArgumentIndex = 2 |
| |
| for assertion in assertions { |
| expectTrue(currentArgumentIndex < buffer.count) |
| |
| assertion(currentArgumentIndex) |
| // Advance the index to the next argument by adding the size of the |
| // current argument and the two bytes of headers. |
| currentArgumentIndex += 2 + Int(buffer[currentArgumentIndex + 1]) |
| } |
| } |
| |
| /// Check a sequence of assertions on the arguments stored in the byte buffer. |
| internal func checkArguments(_ assertions: (Int) -> Void ...) { |
| checkArguments(assertions) |
| } |
| } |
| |
| InterpolationTestSuite.test("integer literal") { |
| _osLogTestHelper( |
| "An integer literal \(10)", |
| assertion: { (formatString, buffer) in |
| expectEqual( |
| "An integer literal %ld", |
| formatString) |
| |
| let bufferChecker = OSLogBufferChecker(buffer) |
| bufferChecker.checkSummaryBytes( |
| argumentCount: 1, |
| hasPrivate: false, |
| hasNonScalar: false) |
| |
| bufferChecker.checkArguments({ |
| bufferChecker.checkInt(startIndex: $0, flag: .autoFlag, expectedInt: 10) |
| }) |
| }) |
| } |
| |
| InterpolationTestSuite.test("integer with formatting") { |
| _osLogTestHelper( |
| "Minimum integer value: \(UInt.max, format: .hex)", |
| assertion: { (formatString, buffer) in |
| expectEqual( |
| "Minimum integer value: %lx", |
| formatString) |
| |
| let bufferChecker = OSLogBufferChecker(buffer) |
| bufferChecker.checkSummaryBytes( |
| argumentCount: 1, |
| hasPrivate: false, |
| hasNonScalar: false) |
| |
| bufferChecker.checkArguments({ |
| bufferChecker.checkInt( |
| startIndex: $0, |
| flag: .autoFlag, |
| expectedInt: UInt.max) |
| }) |
| }) |
| } |
| |
| InterpolationTestSuite.test("integer with privacy and formatting") { |
| let addr: UInt = 0x7afebabe |
| _osLogTestHelper( |
| "Access to invalid address: \(addr, format: .hex, privacy: .private)", |
| assertion: { (formatString, buffer) in |
| expectEqual( |
| "Access to invalid address: %{private}lx", |
| formatString) |
| |
| let bufferChecker = OSLogBufferChecker(buffer) |
| bufferChecker.checkSummaryBytes( |
| argumentCount: 1, |
| hasPrivate: true, |
| hasNonScalar: false) |
| |
| bufferChecker.checkArguments({ |
| bufferChecker.checkInt( |
| startIndex: $0, |
| flag: .privateFlag, |
| expectedInt: addr) |
| }) |
| }) |
| } |
| |
| InterpolationTestSuite.test("test multiple arguments") { |
| let filePerms: UInt = 0o777 |
| let pid = 122225 |
| let privateID = 0x79abcdef |
| |
| _osLogTestHelper( |
| """ |
| Access prevented: process \(pid, privacy: .public) initiated by \ |
| user: \(privateID, privacy: .private) attempted resetting \ |
| permissions to \(filePerms, format: .octal) |
| """, |
| assertion: { (formatString, buffer) in |
| expectEqual( |
| """ |
| Access prevented: process %{public}ld initiated by \ |
| user: %{private}ld attempted resetting permissions \ |
| to %lo |
| """, |
| formatString) |
| |
| let bufferChecker = OSLogBufferChecker(buffer) |
| bufferChecker.checkSummaryBytes( |
| argumentCount: 3, |
| hasPrivate: true, |
| hasNonScalar: false) |
| |
| bufferChecker.checkArguments( |
| { |
| bufferChecker.checkInt( |
| startIndex: $0, |
| flag: .publicFlag, |
| expectedInt: pid) |
| }, |
| { |
| bufferChecker.checkInt( |
| startIndex: $0, |
| flag: .privateFlag, |
| expectedInt: privateID) |
| }, |
| { |
| bufferChecker.checkInt( |
| startIndex: $0, |
| flag: .autoFlag, |
| expectedInt: filePerms) |
| }) |
| }) |
| } |
| |
| InterpolationTestSuite.test("interpolation of too many arguments") { |
| // The following string interpolation has 49 interpolated values. Only 48 |
| // of these must be present in the generated format string and byte buffer. |
| _osLogTestHelper( |
| """ |
| \(1) \(1) \(1) \(1) \(1) \(1) \(1) \(1) \(1) \(1) \(1) \(1) \(1) \(1) \ |
| \(1) \(1) \(1) \(1) \(1) \(1) \(1) \(1) \(1) \(1) \(1) \(1) \(1) \(1) \ |
| \(1) \(1) \(1) \(1) \(1) \(1) \(1) \(1) \(1) \(1) \(1) \(1) \(1) \(1) \ |
| \(1) \(1) \(1) \(1) \(1) \(1) \(1) |
| """, |
| assertion: { (formatString, buffer) in |
| expectEqual( |
| String( |
| repeating: "%ld ", |
| count: Int(maxOSLogArgumentCount)), |
| formatString) |
| |
| let bufferChecker = OSLogBufferChecker(buffer) |
| bufferChecker.checkSummaryBytes( |
| argumentCount: maxOSLogArgumentCount, |
| hasPrivate: false, |
| hasNonScalar: false) |
| |
| bufferChecker.checkArguments( |
| Array( |
| repeating: { |
| bufferChecker.checkInt( |
| startIndex: $0, |
| flag: .autoFlag, |
| expectedInt: 1) }, |
| count: Int(maxOSLogArgumentCount)) |
| ) |
| }) |
| } |
| |
| InterpolationTestSuite.test("string interpolations with percents") { |
| _osLogTestHelper( |
| "a = (c % d)%%", |
| assertion: { (formatString, buffer) in |
| |
| expectEqual("a = (c %% d)%%%%", formatString) |
| |
| let bufferChecker = OSLogBufferChecker(buffer) |
| bufferChecker.checkSummaryBytes( |
| argumentCount: 0, |
| hasPrivate: false, |
| hasNonScalar: false) |
| }) |
| } |
| |
| InterpolationTestSuite.test("integer types") { |
| _osLogTestHelper("Int32 max: \(Int32.max)") { |
| (formatString, buffer) in |
| expectEqual("Int32 max: %d", formatString) |
| |
| let bufferChecker = OSLogBufferChecker(buffer) |
| bufferChecker.checkSummaryBytes( |
| argumentCount: 1, |
| hasPrivate: false, |
| hasNonScalar: false) |
| |
| bufferChecker.checkArguments({ |
| bufferChecker.checkInt( |
| startIndex: $0, |
| flag: .autoFlag, |
| expectedInt: Int32.max) |
| }) |
| } |
| } |
| |
| InterpolationTestSuite.test("string arguments") { |
| let small = "a" |
| let large = "this is a large string" |
| _osLogTestHelper( |
| "small: \(small, privacy: .public) large: \(large)") { |
| (formatString, buffer) in |
| expectEqual("small: %{public}s large: %s", formatString) |
| |
| let bufferChecker = OSLogBufferChecker(buffer) |
| bufferChecker.checkSummaryBytes( |
| argumentCount: 2, |
| hasPrivate: false, |
| hasNonScalar: true |
| ) |
| |
| bufferChecker.checkArguments({ |
| bufferChecker.checkString( |
| startIndex: $0, |
| flag: .publicFlag, |
| expectedString: small) |
| }, |
| { bufferChecker.checkString( |
| startIndex: $0, |
| flag: .autoFlag, |
| expectedString: large) |
| }) |
| } |
| } |
| |
| InterpolationTestSuite.test("dynamic strings") { |
| let concatString = "hello" + " - " + "world" |
| let interpolatedString = "\(31) trillion digits of pi are known so far" |
| |
| _osLogTestHelper( |
| """ |
| concat: \(concatString, privacy: .public) \ |
| interpolated: \(interpolatedString, privacy: .private) |
| """) { (formatString, buffer) in |
| expectEqual("concat: %{public}s interpolated: %{private}s", formatString) |
| |
| let bufferChecker = OSLogBufferChecker(buffer) |
| bufferChecker.checkSummaryBytes( |
| argumentCount: 2, |
| hasPrivate: true, |
| hasNonScalar: true |
| ) |
| |
| bufferChecker.checkArguments({ |
| bufferChecker.checkString( |
| startIndex: $0, |
| flag: .publicFlag, |
| expectedString: concatString) |
| }, |
| { bufferChecker.checkString( |
| startIndex: $0, |
| flag: .privateFlag, |
| expectedString: interpolatedString) |
| }) |
| } |
| } |
| |
| InterpolationTestSuite.test("NSObject") { |
| let nsArray: NSArray = [0, 1, 2] |
| let nsDictionary: NSDictionary = [1 : ""] |
| |
| _osLogTestHelper( |
| """ |
| NSArray: \(nsArray, privacy: .public) \ |
| NSDictionary: \(nsDictionary) |
| """) { (formatString, buffer) in |
| expectEqual("NSArray: %{public}@ NSDictionary: %@", formatString) |
| |
| let bufferChecker = OSLogBufferChecker(buffer) |
| bufferChecker.checkSummaryBytes( |
| argumentCount: 2, |
| hasPrivate: false, |
| hasNonScalar: true |
| ) |
| |
| bufferChecker.checkArguments({ |
| bufferChecker.checkNSObject( |
| startIndex: $0, |
| flag: .publicFlag, |
| expectedObject: nsArray) |
| }, |
| { bufferChecker.checkNSObject( |
| startIndex: $0, |
| flag: .autoFlag, |
| expectedObject: nsDictionary) |
| }) |
| } |
| } |
| |
| // A generic function. |
| func toString<T>(_ subject: T?) -> String { |
| return "" |
| } |
| |
| protocol TestProto { |
| } |
| |
| InterpolationTestSuite.test("Interpolation of complex expressions") { |
| class TestClass<T: TestProto>: NSObject { |
| func testFunction() { |
| // The following call should not crash. |
| _osLogTestHelper("A complex expression \(toString(self))") { |
| (formatString, _) in |
| expectEqual("A complex expression %s", formatString) |
| } |
| } |
| } |
| class B : TestProto { } |
| TestClass<B>().testFunction() |
| } |
| |
| InterpolationTestSuite.test("Include prefix formatting option") { |
| let unsignedValue: UInt = 0o171 |
| _osLogTestHelper( |
| "Octal with prefix: \(unsignedValue, format: .octal(includePrefix: true))") { |
| (formatString, buffer) in |
| expectEqual("Octal with prefix: 0o%lo", formatString) |
| } |
| } |
| |
| InterpolationTestSuite.test("Hex with uppercase formatting option") { |
| let unsignedValue: UInt = 0xcafebabe |
| _osLogTestHelper( |
| "Hex with uppercase: \(unsignedValue, format: .hex(uppercase: true))") { |
| (formatString, buffer) in |
| expectEqual("Hex with uppercase: %lX", formatString) |
| } |
| } |
| |
| InterpolationTestSuite.test("Integer with explicit positive sign") { |
| let posValue = Int.max |
| _osLogTestHelper( |
| "\(posValue, format: .decimal(explicitPositiveSign: true))") { |
| (formatString, buffer) in |
| expectEqual("%+ld", formatString) |
| } |
| } |
| |
| InterpolationTestSuite.test("Unsigned integer with explicit positive sign") { |
| let posValue = UInt.max |
| _osLogTestHelper( |
| "\(posValue, format: .decimal(explicitPositiveSign: true))") { |
| (formatString, buffer) in |
| expectEqual("+%lu", formatString) |
| } |
| } |
| |
| InterpolationTestSuite.test("Integer formatting with precision") { |
| let intValue = 1200 |
| _osLogTestHelper( |
| "\(intValue, format: .decimal(minDigits: 10))") { |
| (formatString, buffer) in |
| expectEqual("%.*ld", formatString) |
| |
| // The buffer should contain two arguments: precision and the actual argument. |
| let bufferChecker = OSLogBufferChecker(buffer) |
| bufferChecker.checkSummaryBytes( |
| argumentCount: 2, |
| hasPrivate: false, |
| hasNonScalar: false |
| ) |
| bufferChecker.checkArguments( |
| { bufferChecker.checkCount( |
| startIndex: $0, |
| expectedCount: 10, |
| precision: true) |
| }, |
| { bufferChecker.checkInt( |
| startIndex: $0, |
| flag: .autoFlag, |
| expectedInt: intValue) |
| }) |
| } |
| } |
| |
| InterpolationTestSuite.test("Integer formatting with alignment") { |
| let intValue = 10 |
| _osLogTestHelper( |
| """ |
| \(intValue, align: .right(columns: 10)) \ |
| \(intValue, align: .left(columns: 5), privacy: .private) |
| """) { |
| (formatString, buffer) in |
| expectEqual("%*ld %{private}-*ld", formatString) |
| } |
| } |
| |
| InterpolationTestSuite.test("Integer formatting with precision and alignment") { |
| let intValue = 1200 |
| _osLogTestHelper( |
| "\(intValue, format: .decimal(minDigits: 10), align: .left(columns: 7))") { |
| (formatString, buffer) in |
| expectEqual("%-*.*ld", formatString) |
| |
| // The buffer must contain three arguments: width, precision, and the argument. |
| let bufferChecker = OSLogBufferChecker(buffer) |
| bufferChecker.checkSummaryBytes( |
| argumentCount: 3, |
| hasPrivate: false, |
| hasNonScalar: false |
| ) |
| bufferChecker.checkArguments( |
| { bufferChecker.checkCount( |
| startIndex: $0, |
| expectedCount: 7, |
| precision: false) |
| }, |
| { bufferChecker.checkCount( |
| startIndex: $0, |
| expectedCount: 10, |
| precision: true) |
| }, |
| { bufferChecker.checkInt( |
| startIndex: $0, |
| flag: .autoFlag, |
| expectedInt: intValue) |
| }) |
| } |
| } |
| |
| InterpolationTestSuite.test("String with alignment") { |
| let smallString = "a" + "b" |
| let concatString = "hello" + " - " + "world" |
| _osLogTestHelper( |
| """ |
| \(smallString, align: .right(columns: 10)) \ |
| \(concatString, align: .left(columns: 7), privacy: .public) |
| """) { |
| (formatString, buffer) in |
| expectEqual("%*s %{public}-*s", formatString) |
| } |
| } |
| |
| InterpolationTestSuite.test("Floats and Doubles") { |
| let x = 1.2 + 0.5 |
| let pi: Double = 3.141593 |
| let floatPi: Float = 3.141593 |
| _osLogTestHelper( |
| """ |
| A double value: \(x, privacy: .private) \ |
| pi as double: \(pi), pi as float: \(floatPi) |
| """) { |
| (formatString, buffer) in |
| expectEqual( |
| """ |
| A double value: %{private}f pi as double: %f, \ |
| pi as float: %f |
| """, |
| formatString) |
| |
| let bufferChecker = OSLogBufferChecker(buffer) |
| bufferChecker.checkSummaryBytes( |
| argumentCount: 3, |
| hasPrivate: true, |
| hasNonScalar: false) |
| |
| bufferChecker.checkArguments( |
| { bufferChecker.checkDouble( |
| startIndex: $0, |
| flag: .privateFlag, |
| expectedValue: x) |
| }, |
| { bufferChecker.checkDouble( |
| startIndex: $0, |
| flag: .autoFlag, |
| expectedValue: pi) |
| }, |
| { bufferChecker.checkDouble( |
| startIndex: $0, |
| flag: .autoFlag, |
| expectedValue: Double(floatPi)) |
| }) |
| } |
| } |