-
-
Notifications
You must be signed in to change notification settings - Fork 8
JavaScript persistence #101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| extension Array: ConvertibleToJSValue where Element: ConvertibleToJSValue { | ||
| @inlinable public var jsValue: JSValue { | ||
| .object(.array(self.map(\.jsValue))) | ||
| } | ||
| } | ||
| extension Array: ConstructibleFromJSValue where Element: ConstructibleFromJSValue { | ||
| @inlinable public static func construct(from value: JSValue) -> [Element]? { | ||
| guard case .object(let object) = value, object.isArray else { | ||
| return nil | ||
| } | ||
|
|
||
| var elements: [Element] = [] | ||
| ; elements.reserveCapacity(object.buffer.count) | ||
| for element: JSValue in object.buffer { | ||
| guard let element: Element = .construct(from: element) else { | ||
| return nil | ||
| } | ||
| elements.append(element) | ||
| } | ||
| return elements | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| extension BinaryFloatingPoint where Self: ConstructibleFromJSValue { | ||
| @inlinable public static func construct(from value: JSValue) -> Self? { | ||
| switch value { | ||
| case .number(let value): | ||
| return Self.init(value) | ||
| case .bigInt(let value): | ||
| return Self.init(exactly: value.int128) | ||
| default: | ||
| return nil | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| extension Bool: ConvertibleToJSValue { | ||
| @inlinable public var jsValue: JSValue { .boolean(self) } | ||
| } | ||
| extension Bool: ConstructibleFromJSValue { | ||
| @inlinable public static func construct(from value: JSValue) -> Bool? { value.boolean } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| public protocol ConstructibleFromJSValue { | ||
| static func construct(from value: JSValue) -> Self? | ||
| } | ||
| extension ConstructibleFromJSValue where Self: SignedInteger { | ||
| @inlinable public static func construct(from value: JSValue) -> Self? { | ||
| switch value { | ||
| case .number(let value): .init(exactly: value) | ||
| case .bigInt(let value): .init(exactly: value.int128) | ||
| default: nil | ||
| } | ||
| } | ||
| } | ||
| extension ConstructibleFromJSValue where Self: UnsignedInteger { | ||
| @inlinable public static func construct(from value: JSValue) -> Self? { | ||
| switch value { | ||
| case .number(let number): Self.init(exactly: number) | ||
| case .bigInt(let bigInt): Self.init(exactly: bigInt.int128) | ||
| default: nil | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| public protocol ConvertibleToJSValue { | ||
| var jsValue: JSValue { get } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| extension Double: ConstructibleFromJSValue {} | ||
| extension Double: ConvertibleToJSValue { | ||
| @inlinable public var jsValue: JSValue { .number(self) } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| extension Float: ConstructibleFromJSValue {} | ||
| extension Float: ConvertibleToJSValue { | ||
| @inlinable public var jsValue: JSValue { .number(Double.init(self)) } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| extension Int: ConstructibleFromJSValue {} | ||
| extension Int: ConvertibleToJSValue { | ||
| @inlinable public var jsValue: JSValue { | ||
| if let double: Double = .init(exactly: self) { | ||
| .number(double) | ||
| } else { | ||
| .bigInt(JSBigInt.init(int128: Int128.init(self))) | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| extension Int16: ConstructibleFromJSValue {} | ||
| extension Int16: ConvertibleToJSValue { | ||
| @inlinable public var jsValue: JSValue { .number(Double.init(self)) } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| extension Int32: ConstructibleFromJSValue {} | ||
| extension Int32: ConvertibleToJSValue { | ||
| @inlinable public var jsValue: JSValue { .number(Double.init(self)) } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| extension Int64: ConstructibleFromJSValue {} | ||
| extension Int64: ConvertibleToJSValue { | ||
| @inlinable public var jsValue: JSValue { | ||
| .bigInt(JSBigInt.init(int128: Int128.init(self))) | ||
| } | ||
|
Comment on lines
+3
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This implementation unconditionally converts @inlinable public var jsValue: JSValue {
if let double: Double = .init(exactly: self) {
.number(double)
} else {
.bigInt(JSBigInt.init(int128: Int128.init(self)))
}
} |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| extension Int8: ConstructibleFromJSValue {} | ||
| extension Int8: ConvertibleToJSValue { | ||
| @inlinable public var jsValue: JSValue { .number(Double.init(self)) } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| public final class JSBigInt { | ||
| @usableFromInline let int128: Int128 | ||
|
|
||
| @inlinable init(int128: Int128) { | ||
| self.int128 = int128 | ||
| } | ||
| } | ||
| extension JSBigInt: Equatable { | ||
| @inlinable public static func == (a: JSBigInt, b: JSBigInt) -> Bool { | ||
| a.int128 == b.int128 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,93 @@ | ||||||||||||||||||
| import JSON | ||||||||||||||||||
|
|
||||||||||||||||||
| public final class JSObject { | ||||||||||||||||||
| @usableFromInline var object: [String: JSValue] | ||||||||||||||||||
| @usableFromInline var buffer: [JSValue] | ||||||||||||||||||
| public let isArray: Bool | ||||||||||||||||||
|
|
||||||||||||||||||
| @inlinable init(object: [String: JSValue], buffer: [JSValue], isArray: Bool) { | ||||||||||||||||||
| self.object = object | ||||||||||||||||||
| self.buffer = buffer | ||||||||||||||||||
| self.isArray = isArray | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| extension JSObject { | ||||||||||||||||||
| @inlinable public static func object(_ properties: [String: JSValue] = [:]) -> JSObject { | ||||||||||||||||||
| return JSObject(object: properties, buffer: [], isArray: false) | ||||||||||||||||||
| } | ||||||||||||||||||
| @inlinable public static func array(_ elements: [JSValue] = []) -> JSObject { | ||||||||||||||||||
| return JSObject(object: [:], buffer: elements, isArray: true) | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| extension JSObject { | ||||||||||||||||||
| public static func json(_ json: JSON.Object) throws -> JSObject { | ||||||||||||||||||
| var object: [String: JSValue] = .init(minimumCapacity: json.fields.count) | ||||||||||||||||||
| for (key, value): (JSON.Key, JSON.Node) in json.fields { | ||||||||||||||||||
| if case _? = object.updateValue(try .json(value), forKey: key.rawValue) { | ||||||||||||||||||
| throw JSON.ObjectKeyError<JSON.Key>.duplicate(key) | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| return .object(object) | ||||||||||||||||||
| } | ||||||||||||||||||
| public static func json(_ json: JSON.Array) throws -> JSObject { | ||||||||||||||||||
| return .array(try json.elements.map(JSValue.json(_:))) | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| extension JSObject: ConvertibleToJSValue { | ||||||||||||||||||
| @inlinable public var jsValue: JSValue { .object(self) } | ||||||||||||||||||
| } | ||||||||||||||||||
| extension JSObject: ConstructibleFromJSValue { | ||||||||||||||||||
| @inlinable public static func construct(from value: JSValue) -> JSObject? { value.object } | ||||||||||||||||||
| } | ||||||||||||||||||
| extension JSObject { | ||||||||||||||||||
| @inlinable public var properties: [String: JSValue] { self.object } | ||||||||||||||||||
| @inlinable public func push(_ value: JSValue) { | ||||||||||||||||||
| self.buffer.append(value) | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| extension JSObject { | ||||||||||||||||||
| @inlinable public subscript(index: Int) -> JSValue { | ||||||||||||||||||
| get { | ||||||||||||||||||
| self.buffer.indices.contains(index) ? self.buffer[index] : .undefined | ||||||||||||||||||
| } | ||||||||||||||||||
| set(value) { | ||||||||||||||||||
| if self.buffer.endIndex < index { | ||||||||||||||||||
| let count: Int = index.distance(to: self.buffer.endIndex) | ||||||||||||||||||
| self.buffer.append(contentsOf: repeatElement(.undefined, count: count)) | ||||||||||||||||||
| self.buffer.append(value) | ||||||||||||||||||
|
Comment on lines
+54
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The "subscript(index: Int)" setter allows setting a value at an arbitrary index. If the index is significantly larger than the current buffer size, this can lead to massive memory allocations and an Out-of-Memory (OOM) condition, crashing the application, thus creating a Denial of Service vulnerability. Additionally, the current logic for setting an element at a given index has a critical bug where passing a negative "count" to "repeatElement" will cause a fatal error and crash the application. |
||||||||||||||||||
| } else if | ||||||||||||||||||
| self.buffer.endIndex == index { | ||||||||||||||||||
| self.buffer.append(value) | ||||||||||||||||||
| } else { | ||||||||||||||||||
| self.buffer[index] = value | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| @inlinable public subscript(key: JSString) -> JSValue { | ||||||||||||||||||
| get { | ||||||||||||||||||
| if self.isArray, key.string == "length" { | ||||||||||||||||||
| return .number(Double.init(self.buffer.count)) | ||||||||||||||||||
| } else { | ||||||||||||||||||
| return self.object[key.string] ?? .undefined | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| set(value) { | ||||||||||||||||||
| if self.isArray, key.string == "length" { | ||||||||||||||||||
| guard | ||||||||||||||||||
| let length: Int = .construct(from: value) else { | ||||||||||||||||||
| fatalError("Invalid array length") | ||||||||||||||||||
| } | ||||||||||||||||||
|
Comment on lines
+76
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using "fatalError" when setting the "length" property of a "JSObject" that is an array is dangerous. If the provided value cannot be converted to an "Int", it will crash the entire application. This allows an attacker to trigger a Denial of Service. JavaScript engines typically handle such invalid assignments more gracefully (e.g., throwing a "RangeError" or ignoring the assignment) rather than terminating the process. It is recommended to handle this error gracefully, for example, by ignoring the invalid assignment.
Suggested change
|
||||||||||||||||||
| if length < self.buffer.count { | ||||||||||||||||||
| self.buffer.removeLast(self.buffer.count - length) | ||||||||||||||||||
| } else if length > self.buffer.count { | ||||||||||||||||||
| self.buffer.reserveCapacity(length) | ||||||||||||||||||
| self.buffer.append( | ||||||||||||||||||
| contentsOf: repeatElement(.undefined, count: length - self.buffer.count) | ||||||||||||||||||
| ) | ||||||||||||||||||
|
Comment on lines
+82
to
+86
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||||||||||||||||||
| } | ||||||||||||||||||
| } else { | ||||||||||||||||||
| self.object[key.string] = value | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| @frozen public struct JSString: Equatable { | ||
| @usableFromInline let string: String | ||
|
|
||
| @inlinable public init(_ string: String) { | ||
| self.string = string | ||
| } | ||
| } | ||
| extension JSString: ExpressibleByStringLiteral { | ||
| @inlinable public init(stringLiteral: String) { | ||
| self.init(stringLiteral) | ||
| } | ||
| } | ||
| extension JSString: CustomStringConvertible, LosslessStringConvertible { | ||
| @inlinable public var description: String { self.string } | ||
| } | ||
| extension JSString: ConvertibleToJSValue { | ||
| @inlinable public var jsValue: JSValue { .string(self) } | ||
| } | ||
| extension JSString: ConstructibleFromJSValue { | ||
| @inlinable public static func construct(from value: JSValue) -> JSString? { | ||
| guard case .string(let string) = value else { | ||
| return nil | ||
| } | ||
| return string | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| public final class JSSymbol { | ||
| public let name: String? | ||
|
|
||
| @inlinable init(name: String?) { | ||
| self.name = name | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This leading semicolon is unnecessary. While it doesn't affect the logic, it's unconventional and harms code readability. It should be removed to adhere to standard Swift styling.