Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ let package: Package = .init(
.library(name: "JSON", targets: ["JSON"]),
.library(name: "JSONAST", targets: ["JSONAST"]),
.library(name: "JSONLegacy", targets: ["JSONLegacy"]),
.library(name: "JavaScriptPersistence", targets: ["JavaScriptPersistence"]),
],
dependencies: [
.package(url: "https://github.com/tayloraswift/swift-grammar", from: "0.5.0"),
Expand Down Expand Up @@ -55,6 +56,13 @@ let package: Package = .init(
]
),

.target(
name: "JavaScriptPersistence",
dependencies: [
.target(name: "JSON"),
]
),

.testTarget(
name: "JSONTests",
dependencies: [
Expand Down
22 changes: 22 additions & 0 deletions Sources/JavaScriptPersistence/Array (ext).swift
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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
; elements.reserveCapacity(object.buffer.count)
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
}
}
12 changes: 12 additions & 0 deletions Sources/JavaScriptPersistence/BinaryFloatingPoint (ext).swift
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
}
}
}
6 changes: 6 additions & 0 deletions Sources/JavaScriptPersistence/Bool (ext).swift
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 }
}
21 changes: 21 additions & 0 deletions Sources/JavaScriptPersistence/ConstructibleFromJSValue.swift
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
}
}
}
3 changes: 3 additions & 0 deletions Sources/JavaScriptPersistence/ConvertibleToJSValue.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
public protocol ConvertibleToJSValue {
var jsValue: JSValue { get }
}
4 changes: 4 additions & 0 deletions Sources/JavaScriptPersistence/Double (ext).swift
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) }
}
4 changes: 4 additions & 0 deletions Sources/JavaScriptPersistence/Float (ext).swift
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)) }
}
10 changes: 10 additions & 0 deletions Sources/JavaScriptPersistence/Int (ext).swift
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)))
}
}
}
4 changes: 4 additions & 0 deletions Sources/JavaScriptPersistence/Int16 (ext).swift
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)) }
}
4 changes: 4 additions & 0 deletions Sources/JavaScriptPersistence/Int32 (ext).swift
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)) }
}
6 changes: 6 additions & 0 deletions Sources/JavaScriptPersistence/Int64 (ext).swift
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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This implementation unconditionally converts Int64 to JSBigInt. However, Int64 values up to 2^53 can be represented exactly as a Double (JSValue.number). For consistency with the implementation for Int and to better align with JavaScript's number handling, you should check if the value can be represented as a Double first, and only fall back to JSBigInt if it cannot.

    @inlinable public var jsValue: JSValue {
        if let double: Double = .init(exactly: self) {
            .number(double)
        } else {
            .bigInt(JSBigInt.init(int128: Int128.init(self)))
        }
    }

}
4 changes: 4 additions & 0 deletions Sources/JavaScriptPersistence/Int8 (ext).swift
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)) }
}
12 changes: 12 additions & 0 deletions Sources/JavaScriptPersistence/JSBigInt.swift
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
}
}
93 changes: 93 additions & 0 deletions Sources/JavaScriptPersistence/JSObject.swift
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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

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
guard
let length: Int = .construct(from: value) else {
fatalError("Invalid array length")
}
guard
let length: Int = .construct(from: value) else {
return
}

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The subscript(key: JSString) setter for the "length" property of an array allows an attacker to set an arbitrary length. If the new length is much larger than the current buffer size, the code attempts to expand the buffer and fill it with .undefined values. This can lead to an OOM condition and a Denial of Service.

}
} else {
self.object[key.string] = value
}
}
}
}
26 changes: 26 additions & 0 deletions Sources/JavaScriptPersistence/JSString.swift
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
}
}
7 changes: 7 additions & 0 deletions Sources/JavaScriptPersistence/JSSymbol.swift
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
}
}
Loading