mirror of
https://github.com/oven-sh/bun
synced 2026-02-27 03:57:23 +01:00
Compare commits
9 Commits
chloe/hmr1
...
jarred/shr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e2ed555b7 | ||
|
|
84a21234d4 | ||
|
|
fefdaefb97 | ||
|
|
50eaea19cb | ||
|
|
438d8555c6 | ||
|
|
163a51c0f6 | ||
|
|
8df7064f73 | ||
|
|
99ee90a58f | ||
|
|
46c43d954c |
488
.cursor/rules/zig-javascriptcore-classes.mdc
Normal file
488
.cursor/rules/zig-javascriptcore-classes.mdc
Normal file
@@ -0,0 +1,488 @@
|
||||
---
|
||||
description: How Zig works with JavaScriptCore bindings generator
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
# Bun's JavaScriptCore Class Bindings Generator
|
||||
|
||||
This document explains how Bun's class bindings generator works to bridge Zig and JavaScript code through JavaScriptCore (JSC).
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
Bun's binding system creates a seamless bridge between JavaScript and Zig, allowing Zig implementations to be exposed as JavaScript classes. The system has several key components:
|
||||
|
||||
1. **Zig Implementation** (.zig files)
|
||||
2. **JavaScript Interface Definition** (.classes.ts files)
|
||||
3. **Generated Code** (C++/Zig files that connect everything)
|
||||
|
||||
## Class Definition Files
|
||||
|
||||
### JavaScript Interface (.classes.ts)
|
||||
|
||||
The `.classes.ts` files define the JavaScript API using a declarative approach:
|
||||
|
||||
```typescript
|
||||
// Example: encoding.classes.ts
|
||||
define({
|
||||
name: "TextDecoder",
|
||||
constructor: true,
|
||||
JSType: "object",
|
||||
finalize: true,
|
||||
proto: {
|
||||
decode: {
|
||||
// Function definition
|
||||
args: 1,
|
||||
},
|
||||
encoding: {
|
||||
// Getter with caching
|
||||
getter: true,
|
||||
cache: true,
|
||||
},
|
||||
fatal: {
|
||||
// Read-only property
|
||||
getter: true,
|
||||
},
|
||||
ignoreBOM: {
|
||||
// Read-only property
|
||||
getter: true,
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Each class definition specifies:
|
||||
- The class name
|
||||
- Whether it has a constructor
|
||||
- JavaScript type (object, function, etc.)
|
||||
- Properties and methods in the `proto` field
|
||||
- Caching strategy for properties
|
||||
- Finalization requirements
|
||||
|
||||
### Zig Implementation (.zig)
|
||||
|
||||
The Zig files implement the native functionality:
|
||||
|
||||
```zig
|
||||
// Example: TextDecoder.zig
|
||||
pub const TextDecoder = struct {
|
||||
// Internal state
|
||||
encoding: []const u8,
|
||||
fatal: bool,
|
||||
ignoreBOM: bool,
|
||||
|
||||
// Use generated bindings
|
||||
pub usingnamespace JSC.Codegen.JSTextDecoder;
|
||||
pub usingnamespace bun.New(@This());
|
||||
|
||||
// Constructor implementation - note use of globalObject
|
||||
pub fn constructor(
|
||||
globalObject: *JSGlobalObject,
|
||||
callFrame: *JSC.CallFrame,
|
||||
) bun.JSError!*TextDecoder {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
// Prototype methods - note return type includes JSError
|
||||
pub fn decode(
|
||||
this: *TextDecoder,
|
||||
globalObject: *JSGlobalObject,
|
||||
callFrame: *JSC.CallFrame,
|
||||
) bun.JSError!JSC.JSValue {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
// Getters
|
||||
pub fn getEncoding(this: *TextDecoder, globalObject: *JSGlobalObject) JSC.JSValue {
|
||||
return JSC.JSValue.createStringFromUTF8(globalObject, this.encoding);
|
||||
}
|
||||
|
||||
pub fn getFatal(this: *TextDecoder, globalObject: *JSGlobalObject) JSC.JSValue {
|
||||
return JSC.JSValue.jsBoolean(this.fatal);
|
||||
}
|
||||
|
||||
// Cleanup - note standard pattern of using deinit/deref
|
||||
pub fn deinit(this: *TextDecoder) void {
|
||||
// Release any retained resources
|
||||
}
|
||||
|
||||
pub fn finalize(this: *TextDecoder) void {
|
||||
this.deinit();
|
||||
// Or sometimes this is used to free memory instead
|
||||
bun.default_allocator.destroy(this);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Key components in the Zig file:
|
||||
- The struct containing native state
|
||||
- `usingnamespace JSC.Codegen.JS<ClassName>` to include generated code
|
||||
- `usingnamespace bun.New(@This())` for object creation helpers
|
||||
- Constructor and methods using `bun.JSError!JSValue` return type for proper error handling
|
||||
- Consistent use of `globalObject` parameter name instead of `ctx`
|
||||
- Methods matching the JavaScript interface
|
||||
- Getters/setters for properties
|
||||
- Proper resource cleanup pattern with `deinit()` and `finalize()`
|
||||
|
||||
## Code Generation System
|
||||
|
||||
The binding generator produces C++ code that connects JavaScript and Zig:
|
||||
|
||||
1. **JSC Class Structure**: Creates C++ classes for the JS object, prototype, and constructor
|
||||
2. **Memory Management**: Handles GC integration through JSC's WriteBarrier
|
||||
3. **Method Binding**: Connects JS function calls to Zig implementations
|
||||
4. **Type Conversion**: Converts between JS values and Zig types
|
||||
5. **Property Caching**: Implements the caching system for properties
|
||||
|
||||
The generated C++ code includes:
|
||||
- A JSC wrapper class (`JSTextDecoder`)
|
||||
- A prototype class (`JSTextDecoderPrototype`)
|
||||
- A constructor function (`JSTextDecoderConstructor`)
|
||||
- Function bindings (`TextDecoderPrototype__decodeCallback`)
|
||||
- Property getters/setters (`TextDecoderPrototype__encodingGetterWrap`)
|
||||
|
||||
## CallFrame Access
|
||||
|
||||
The `CallFrame` object provides access to JavaScript execution context:
|
||||
|
||||
```zig
|
||||
pub fn decode(
|
||||
this: *TextDecoder,
|
||||
globalObject: *JSGlobalObject,
|
||||
callFrame: *JSC.CallFrame
|
||||
) bun.JSError!JSC.JSValue {
|
||||
// Get arguments
|
||||
const input = callFrame.argument(0);
|
||||
const options = callFrame.argument(1);
|
||||
|
||||
// Get this value
|
||||
const thisValue = callFrame.thisValue();
|
||||
|
||||
// Implementation with error handling
|
||||
if (input.isUndefinedOrNull()) {
|
||||
return globalObject.throw("Input cannot be null or undefined", .{});
|
||||
}
|
||||
|
||||
// Return value or throw error
|
||||
return JSC.JSValue.jsString(globalObject, "result");
|
||||
}
|
||||
```
|
||||
|
||||
CallFrame methods include:
|
||||
- `argument(i)`: Get the i-th argument
|
||||
- `argumentCount()`: Get the number of arguments
|
||||
- `thisValue()`: Get the `this` value
|
||||
- `callee()`: Get the function being called
|
||||
|
||||
## Property Caching and GC-Owned Values
|
||||
|
||||
The `cache: true` option in property definitions enables JSC's WriteBarrier to efficiently store values:
|
||||
|
||||
```typescript
|
||||
encoding: {
|
||||
getter: true,
|
||||
cache: true, // Enable caching
|
||||
}
|
||||
```
|
||||
|
||||
### C++ Implementation
|
||||
|
||||
In the generated C++ code, caching uses JSC's WriteBarrier:
|
||||
|
||||
```cpp
|
||||
JSC_DEFINE_CUSTOM_GETTER(TextDecoderPrototype__encodingGetterWrap, (...)) {
|
||||
auto& vm = JSC::getVM(lexicalGlobalObject);
|
||||
Zig::GlobalObject *globalObject = reinterpret_cast<Zig::GlobalObject*>(lexicalGlobalObject);
|
||||
auto throwScope = DECLARE_THROW_SCOPE(vm);
|
||||
JSTextDecoder* thisObject = jsCast<JSTextDecoder*>(JSValue::decode(encodedThisValue));
|
||||
JSC::EnsureStillAliveScope thisArg = JSC::EnsureStillAliveScope(thisObject);
|
||||
|
||||
// Check for cached value and return if present
|
||||
if (JSValue cachedValue = thisObject->m_encoding.get())
|
||||
return JSValue::encode(cachedValue);
|
||||
|
||||
// Get value from Zig implementation
|
||||
JSC::JSValue result = JSC::JSValue::decode(
|
||||
TextDecoderPrototype__getEncoding(thisObject->wrapped(), globalObject)
|
||||
);
|
||||
RETURN_IF_EXCEPTION(throwScope, {});
|
||||
|
||||
// Store in cache for future access
|
||||
thisObject->m_encoding.set(vm, thisObject, result);
|
||||
RELEASE_AND_RETURN(throwScope, JSValue::encode(result));
|
||||
}
|
||||
```
|
||||
|
||||
### Zig Accessor Functions
|
||||
|
||||
For each cached property, the generator creates Zig accessor functions that allow Zig code to work with these GC-owned values:
|
||||
|
||||
```zig
|
||||
// External function declarations
|
||||
extern fn TextDecoderPrototype__encodingSetCachedValue(JSC.JSValue, *JSC.JSGlobalObject, JSC.JSValue) callconv(JSC.conv) void;
|
||||
extern fn TextDecoderPrototype__encodingGetCachedValue(JSC.JSValue) callconv(JSC.conv) JSC.JSValue;
|
||||
|
||||
/// `TextDecoder.encoding` setter
|
||||
/// This value will be visited by the garbage collector.
|
||||
pub fn encodingSetCached(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void {
|
||||
JSC.markBinding(@src());
|
||||
TextDecoderPrototype__encodingSetCachedValue(thisValue, globalObject, value);
|
||||
}
|
||||
|
||||
/// `TextDecoder.encoding` getter
|
||||
/// This value will be visited by the garbage collector.
|
||||
pub fn encodingGetCached(thisValue: JSC.JSValue) ?JSC.JSValue {
|
||||
JSC.markBinding(@src());
|
||||
const result = TextDecoderPrototype__encodingGetCachedValue(thisValue);
|
||||
if (result == .zero)
|
||||
return null;
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
### Benefits of GC-Owned Values
|
||||
|
||||
This system provides several key benefits:
|
||||
|
||||
1. **Automatic Memory Management**: The JavaScriptCore GC tracks and manages these values
|
||||
2. **Proper Garbage Collection**: The WriteBarrier ensures values are properly visited during GC
|
||||
3. **Consistent Access**: Zig code can easily get/set these cached JS values
|
||||
4. **Performance**: Cached values avoid repeated computation or serialization
|
||||
|
||||
### Use Cases
|
||||
|
||||
GC-owned cached values are particularly useful for:
|
||||
|
||||
1. **Computed Properties**: Store expensive computation results
|
||||
2. **Lazily Created Objects**: Create objects only when needed, then cache them
|
||||
3. **References to Other Objects**: Store references to other JS objects that need GC tracking
|
||||
4. **Memoization**: Cache results based on input parameters
|
||||
|
||||
The WriteBarrier mechanism ensures that any JS values stored in this way are properly tracked by the garbage collector.
|
||||
|
||||
## Memory Management and Finalization
|
||||
|
||||
The binding system handles memory management across the JavaScript/Zig boundary:
|
||||
|
||||
1. **Object Creation**: JavaScript `new TextDecoder()` creates both a JS wrapper and a Zig struct
|
||||
2. **Reference Tracking**: JSC's GC tracks all JS references to the object
|
||||
3. **Finalization**: When the JS object is collected, the finalizer releases Zig resources
|
||||
|
||||
Bun uses a consistent pattern for resource cleanup:
|
||||
|
||||
```zig
|
||||
// Resource cleanup method - separate from finalization
|
||||
pub fn deinit(this: *TextDecoder) void {
|
||||
// Release resources like strings
|
||||
this._encoding.deref(); // String deref pattern
|
||||
|
||||
// Free any buffers
|
||||
if (this.buffer) |buffer| {
|
||||
bun.default_allocator.free(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
// Called by the GC when object is collected
|
||||
pub fn finalize(this: *TextDecoder) void {
|
||||
JSC.markBinding(@src()); // For debugging
|
||||
this.deinit(); // Clean up resources
|
||||
bun.default_allocator.destroy(this); // Free the object itself
|
||||
}
|
||||
```
|
||||
|
||||
Some objects that hold references to other JS objects use `.deref()` instead:
|
||||
|
||||
```zig
|
||||
pub fn finalize(this: *SocketAddress) void {
|
||||
JSC.markBinding(@src());
|
||||
this._presentation.deref(); // Release references
|
||||
this.destroy();
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling with JSError
|
||||
|
||||
Bun uses `bun.JSError!JSValue` return type for proper error handling:
|
||||
|
||||
```zig
|
||||
pub fn decode(
|
||||
this: *TextDecoder,
|
||||
globalObject: *JSGlobalObject,
|
||||
callFrame: *JSC.CallFrame
|
||||
) bun.JSError!JSC.JSValue {
|
||||
// Throwing an error
|
||||
if (callFrame.argumentCount() < 1) {
|
||||
return globalObject.throw("Missing required argument", .{});
|
||||
}
|
||||
|
||||
// Or returning a success value
|
||||
return JSC.JSValue.jsString(globalObject, "Success!");
|
||||
}
|
||||
```
|
||||
|
||||
This pattern allows Zig functions to:
|
||||
1. Return JavaScript values on success
|
||||
2. Throw JavaScript exceptions on error
|
||||
3. Propagate errors automatically through the call stack
|
||||
|
||||
## Type Safety and Error Handling
|
||||
|
||||
The binding system includes robust error handling:
|
||||
|
||||
```cpp
|
||||
// Example of type checking in generated code
|
||||
JSTextDecoder* thisObject = jsDynamicCast<JSTextDecoder*>(callFrame->thisValue());
|
||||
if (UNLIKELY(!thisObject)) {
|
||||
scope.throwException(lexicalGlobalObject,
|
||||
Bun::createInvalidThisError(lexicalGlobalObject, callFrame->thisValue(), "TextDecoder"_s));
|
||||
return {};
|
||||
}
|
||||
```
|
||||
|
||||
## Prototypal Inheritance
|
||||
|
||||
The binding system creates proper JavaScript prototype chains:
|
||||
|
||||
1. **Constructor**: JSTextDecoderConstructor with standard .prototype property
|
||||
2. **Prototype**: JSTextDecoderPrototype with methods and properties
|
||||
3. **Instances**: Each JSTextDecoder instance with __proto__ pointing to prototype
|
||||
|
||||
This ensures JavaScript inheritance works as expected:
|
||||
|
||||
```cpp
|
||||
// From generated code
|
||||
void JSTextDecoderConstructor::finishCreation(VM& vm, JSC::JSGlobalObject* globalObject, JSTextDecoderPrototype* prototype)
|
||||
{
|
||||
Base::finishCreation(vm, 0, "TextDecoder"_s, PropertyAdditionMode::WithoutStructureTransition);
|
||||
|
||||
// Set up the prototype chain
|
||||
putDirectWithoutTransition(vm, vm.propertyNames->prototype, prototype, PropertyAttribute::DontEnum | PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly);
|
||||
ASSERT(inherits(info()));
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
The binding system is optimized for performance:
|
||||
|
||||
1. **Direct Pointer Access**: JavaScript objects maintain a direct pointer to Zig objects
|
||||
2. **Property Caching**: WriteBarrier caching avoids repeated native calls for stable properties
|
||||
3. **Memory Management**: JSC garbage collection integrated with Zig memory management
|
||||
4. **Type Conversion**: Fast paths for common JavaScript/Zig type conversions
|
||||
|
||||
## Creating a New Class Binding
|
||||
|
||||
To create a new class binding in Bun:
|
||||
|
||||
1. **Define the class interface** in a `.classes.ts` file:
|
||||
```typescript
|
||||
define({
|
||||
name: "MyClass",
|
||||
constructor: true,
|
||||
finalize: true,
|
||||
proto: {
|
||||
myMethod: {
|
||||
args: 1,
|
||||
},
|
||||
myProperty: {
|
||||
getter: true,
|
||||
cache: true,
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
2. **Implement the native functionality** in a `.zig` file:
|
||||
```zig
|
||||
pub const MyClass = struct {
|
||||
// State
|
||||
value: []const u8,
|
||||
|
||||
// Generated bindings
|
||||
pub usingnamespace JSC.Codegen.JSMyClass;
|
||||
pub usingnamespace bun.New(@This());
|
||||
|
||||
// Constructor
|
||||
pub fn constructor(
|
||||
globalObject: *JSGlobalObject,
|
||||
callFrame: *JSC.CallFrame,
|
||||
) bun.JSError!*MyClass {
|
||||
const arg = callFrame.argument(0);
|
||||
// Implementation
|
||||
}
|
||||
|
||||
// Method
|
||||
pub fn myMethod(
|
||||
this: *MyClass,
|
||||
globalObject: *JSGlobalObject,
|
||||
callFrame: *JSC.CallFrame,
|
||||
) bun.JSError!JSC.JSValue {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
// Getter
|
||||
pub fn getMyProperty(this: *MyClass, globalObject: *JSGlobalObject) JSC.JSValue {
|
||||
return JSC.JSValue.jsString(globalObject, this.value);
|
||||
}
|
||||
|
||||
// Resource cleanup
|
||||
pub fn deinit(this: *MyClass) void {
|
||||
// Clean up resources
|
||||
}
|
||||
|
||||
pub fn finalize(this: *MyClass) void {
|
||||
this.deinit();
|
||||
bun.default_allocator.destroy(this);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
3. **The binding generator** creates all necessary C++ and Zig glue code to connect JavaScript and Zig, including:
|
||||
- C++ class definitions
|
||||
- Method and property bindings
|
||||
- Memory management utilities
|
||||
- GC integration code
|
||||
|
||||
## Generated Code Structure
|
||||
|
||||
The binding generator produces several components:
|
||||
|
||||
### 1. C++ Classes
|
||||
|
||||
For each Zig class, the system generates:
|
||||
|
||||
- **JS<Class>**: Main wrapper that holds a pointer to the Zig object (`JSTextDecoder`)
|
||||
- **JS<Class>Prototype**: Contains methods and properties (`JSTextDecoderPrototype`)
|
||||
- **JS<Class>Constructor**: Implementation of the JavaScript constructor (`JSTextDecoderConstructor`)
|
||||
|
||||
### 2. C++ Methods and Properties
|
||||
|
||||
- **Method Callbacks**: `TextDecoderPrototype__decodeCallback`
|
||||
- **Property Getters/Setters**: `TextDecoderPrototype__encodingGetterWrap`
|
||||
- **Initialization Functions**: `finishCreation` methods for setting up the class
|
||||
|
||||
### 3. Zig Bindings
|
||||
|
||||
- **External Function Declarations**:
|
||||
```zig
|
||||
extern fn TextDecoderPrototype__decode(*TextDecoder, *JSC.JSGlobalObject, *JSC.CallFrame) callconv(JSC.conv) JSC.EncodedJSValue;
|
||||
```
|
||||
|
||||
- **Cached Value Accessors**:
|
||||
```zig
|
||||
pub fn encodingGetCached(thisValue: JSC.JSValue) ?JSC.JSValue { ... }
|
||||
pub fn encodingSetCached(thisValue: JSC.JSValue, globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) void { ... }
|
||||
```
|
||||
|
||||
- **Constructor Helpers**:
|
||||
```zig
|
||||
pub fn create(globalObject: *JSC.JSGlobalObject) bun.JSError!JSC.JSValue { ... }
|
||||
```
|
||||
|
||||
### 4. GC Integration
|
||||
|
||||
- **Memory Cost Calculation**: `estimatedSize` method
|
||||
- **Child Visitor Methods**: `visitChildrenImpl` and `visitAdditionalChildren`
|
||||
- **Heap Analysis**: `analyzeHeap` for debugging memory issues
|
||||
|
||||
This architecture makes it possible to implement high-performance native functionality in Zig while exposing a clean, idiomatic JavaScript API to users.
|
||||
@@ -53,39 +53,39 @@ $ brew install bun
|
||||
|
||||
## Install LLVM
|
||||
|
||||
Bun requires LLVM 18 (`clang` is part of LLVM). This version requirement is to match WebKit (precompiled), as mismatching versions will cause memory allocation failures at runtime. In most cases, you can install LLVM through your system package manager:
|
||||
Bun requires LLVM 19 (`clang` is part of LLVM). This version requirement is to match WebKit (precompiled), as mismatching versions will cause memory allocation failures at runtime. In most cases, you can install LLVM through your system package manager:
|
||||
|
||||
{% codetabs group="os" %}
|
||||
|
||||
```bash#macOS (Homebrew)
|
||||
$ brew install llvm@18
|
||||
$ brew install llvm@19
|
||||
```
|
||||
|
||||
```bash#Ubuntu/Debian
|
||||
$ # LLVM has an automatic installation script that is compatible with all versions of Ubuntu
|
||||
$ wget https://apt.llvm.org/llvm.sh -O - | sudo bash -s -- 18 all
|
||||
$ wget https://apt.llvm.org/llvm.sh -O - | sudo bash -s -- 19 all
|
||||
```
|
||||
|
||||
```bash#Arch
|
||||
$ sudo pacman -S llvm clang18 lld
|
||||
$ sudo pacman -S llvm clang lld
|
||||
```
|
||||
|
||||
```bash#Fedora
|
||||
$ sudo dnf install llvm18 clang18 lld18-devel
|
||||
$ sudo dnf install llvm clang lld-devel
|
||||
```
|
||||
|
||||
```bash#openSUSE Tumbleweed
|
||||
$ sudo zypper install clang18 lld18 llvm18
|
||||
$ sudo zypper install clang19 lld19 llvm19
|
||||
```
|
||||
|
||||
{% /codetabs %}
|
||||
|
||||
If none of the above solutions apply, you will have to install it [manually](https://github.com/llvm/llvm-project/releases/tag/llvmorg-19.1.7).
|
||||
|
||||
Make sure Clang/LLVM 18 is in your path:
|
||||
Make sure Clang/LLVM 19 is in your path:
|
||||
|
||||
```bash
|
||||
$ which clang-18
|
||||
$ which clang-19
|
||||
```
|
||||
|
||||
If not, run this to manually add it:
|
||||
@@ -94,13 +94,13 @@ If not, run this to manually add it:
|
||||
|
||||
```bash#macOS (Homebrew)
|
||||
# use fish_add_path if you're using fish
|
||||
# use path+="$(brew --prefix llvm@18)/bin" if you are using zsh
|
||||
$ export PATH="$(brew --prefix llvm@18)/bin:$PATH"
|
||||
# use path+="$(brew --prefix llvm@19)/bin" if you are using zsh
|
||||
$ export PATH="$(brew --prefix llvm@19)/bin:$PATH"
|
||||
```
|
||||
|
||||
```bash#Arch
|
||||
# use fish_add_path if you're using fish
|
||||
$ export PATH="$PATH:/usr/lib/llvm18/bin"
|
||||
$ export PATH="$PATH:/usr/lib/llvm19/bin"
|
||||
```
|
||||
|
||||
{% /codetabs %}
|
||||
@@ -250,7 +250,7 @@ The issue may manifest when initially running `bun setup` as Clang being unable
|
||||
```
|
||||
The C++ compiler
|
||||
|
||||
"/usr/bin/clang++-18"
|
||||
"/usr/bin/clang++-19"
|
||||
|
||||
is not able to compile a simple test program.
|
||||
```
|
||||
|
||||
2
packages/bun-types/bun.d.ts
vendored
2
packages/bun-types/bun.d.ts
vendored
@@ -1968,7 +1968,7 @@ declare module "bun" {
|
||||
* user: 'dbuser',
|
||||
* password: 'secretpass',
|
||||
* database: 'myapp',
|
||||
* idleTimeout: 30000,
|
||||
* idleTimeout: 30,
|
||||
* max: 20,
|
||||
* onconnect: (client) => {
|
||||
* console.log('Connected to database');
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
638
src/bun.js/api/FFIObject.zig
Normal file
638
src/bun.js/api/FFIObject.zig
Normal file
@@ -0,0 +1,638 @@
|
||||
pub fn newCString(globalThis: *JSGlobalObject, value: JSValue, byteOffset: ?JSValue, lengthValue: ?JSValue) JSC.JSValue {
|
||||
switch (FFIObject.getPtrSlice(globalThis, value, byteOffset, lengthValue)) {
|
||||
.err => |err| {
|
||||
return err;
|
||||
},
|
||||
.slice => |slice| {
|
||||
return bun.String.createUTF8ForJS(globalThis, slice);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub const dom_call = JSC.DOMCall("FFI", @This(), "ptr", JSC.DOMEffect.forRead(.TypedArrayProperties));
|
||||
|
||||
pub fn toJS(globalObject: *JSC.JSGlobalObject) JSC.JSValue {
|
||||
const object = JSC.JSValue.createEmptyObject(globalObject, comptime std.meta.fieldNames(@TypeOf(fields)).len + 2);
|
||||
inline for (comptime std.meta.fieldNames(@TypeOf(fields))) |field| {
|
||||
object.put(
|
||||
globalObject,
|
||||
comptime ZigString.static(field),
|
||||
JSC.createCallback(globalObject, comptime ZigString.static(field), 1, comptime @field(fields, field)),
|
||||
);
|
||||
}
|
||||
|
||||
dom_call.put(globalObject, object);
|
||||
object.put(globalObject, ZigString.static("read"), Reader.toJS(globalObject));
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
pub const Reader = struct {
|
||||
pub const DOMCalls = .{
|
||||
.u8 = JSC.DOMCall("Reader", @This(), "u8", JSC.DOMEffect.forRead(.World)),
|
||||
.u16 = JSC.DOMCall("Reader", @This(), "u16", JSC.DOMEffect.forRead(.World)),
|
||||
.u32 = JSC.DOMCall("Reader", @This(), "u32", JSC.DOMEffect.forRead(.World)),
|
||||
.ptr = JSC.DOMCall("Reader", @This(), "ptr", JSC.DOMEffect.forRead(.World)),
|
||||
.i8 = JSC.DOMCall("Reader", @This(), "i8", JSC.DOMEffect.forRead(.World)),
|
||||
.i16 = JSC.DOMCall("Reader", @This(), "i16", JSC.DOMEffect.forRead(.World)),
|
||||
.i32 = JSC.DOMCall("Reader", @This(), "i32", JSC.DOMEffect.forRead(.World)),
|
||||
.i64 = JSC.DOMCall("Reader", @This(), "i64", JSC.DOMEffect.forRead(.World)),
|
||||
.u64 = JSC.DOMCall("Reader", @This(), "u64", JSC.DOMEffect.forRead(.World)),
|
||||
.intptr = JSC.DOMCall("Reader", @This(), "intptr", JSC.DOMEffect.forRead(.World)),
|
||||
.f32 = JSC.DOMCall("Reader", @This(), "f32", JSC.DOMEffect.forRead(.World)),
|
||||
.f64 = JSC.DOMCall("Reader", @This(), "f64", JSC.DOMEffect.forRead(.World)),
|
||||
};
|
||||
|
||||
pub fn toJS(globalThis: *JSC.JSGlobalObject) JSC.JSValue {
|
||||
const obj = JSC.JSValue.createEmptyObject(globalThis, std.meta.fieldNames(@TypeOf(Reader.DOMCalls)).len);
|
||||
|
||||
inline for (comptime std.meta.fieldNames(@TypeOf(Reader.DOMCalls))) |field| {
|
||||
@field(Reader.DOMCalls, field).put(globalThis, obj);
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
pub fn @"u8"(
|
||||
globalObject: *JSGlobalObject,
|
||||
_: JSValue,
|
||||
arguments: []const JSValue,
|
||||
) bun.JSError!JSValue {
|
||||
if (arguments.len == 0 or !arguments[0].isNumber()) {
|
||||
return globalObject.throwInvalidArguments("Expected a pointer", .{});
|
||||
}
|
||||
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
|
||||
const value = @as(*align(1) u8, @ptrFromInt(addr)).*;
|
||||
return JSValue.jsNumber(value);
|
||||
}
|
||||
pub fn @"u16"(
|
||||
globalObject: *JSGlobalObject,
|
||||
_: JSValue,
|
||||
arguments: []const JSValue,
|
||||
) bun.JSError!JSValue {
|
||||
if (arguments.len == 0 or !arguments[0].isNumber()) {
|
||||
return globalObject.throwInvalidArguments("Expected a pointer", .{});
|
||||
}
|
||||
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
|
||||
const value = @as(*align(1) u16, @ptrFromInt(addr)).*;
|
||||
return JSValue.jsNumber(value);
|
||||
}
|
||||
pub fn @"u32"(
|
||||
globalObject: *JSGlobalObject,
|
||||
_: JSValue,
|
||||
arguments: []const JSValue,
|
||||
) bun.JSError!JSValue {
|
||||
if (arguments.len == 0 or !arguments[0].isNumber()) {
|
||||
return globalObject.throwInvalidArguments("Expected a pointer", .{});
|
||||
}
|
||||
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
|
||||
const value = @as(*align(1) u32, @ptrFromInt(addr)).*;
|
||||
return JSValue.jsNumber(value);
|
||||
}
|
||||
pub fn ptr(
|
||||
globalObject: *JSGlobalObject,
|
||||
_: JSValue,
|
||||
arguments: []const JSValue,
|
||||
) bun.JSError!JSValue {
|
||||
if (arguments.len == 0 or !arguments[0].isNumber()) {
|
||||
return globalObject.throwInvalidArguments("Expected a pointer", .{});
|
||||
}
|
||||
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
|
||||
const value = @as(*align(1) u64, @ptrFromInt(addr)).*;
|
||||
return JSValue.jsNumber(value);
|
||||
}
|
||||
pub fn @"i8"(
|
||||
globalObject: *JSGlobalObject,
|
||||
_: JSValue,
|
||||
arguments: []const JSValue,
|
||||
) bun.JSError!JSValue {
|
||||
if (arguments.len == 0 or !arguments[0].isNumber()) {
|
||||
return globalObject.throwInvalidArguments("Expected a pointer", .{});
|
||||
}
|
||||
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
|
||||
const value = @as(*align(1) i8, @ptrFromInt(addr)).*;
|
||||
return JSValue.jsNumber(value);
|
||||
}
|
||||
pub fn @"i16"(
|
||||
globalObject: *JSGlobalObject,
|
||||
_: JSValue,
|
||||
arguments: []const JSValue,
|
||||
) bun.JSError!JSValue {
|
||||
if (arguments.len == 0 or !arguments[0].isNumber()) {
|
||||
return globalObject.throwInvalidArguments("Expected a pointer", .{});
|
||||
}
|
||||
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
|
||||
const value = @as(*align(1) i16, @ptrFromInt(addr)).*;
|
||||
return JSValue.jsNumber(value);
|
||||
}
|
||||
pub fn @"i32"(
|
||||
globalObject: *JSGlobalObject,
|
||||
_: JSValue,
|
||||
arguments: []const JSValue,
|
||||
) bun.JSError!JSValue {
|
||||
if (arguments.len == 0 or !arguments[0].isNumber()) {
|
||||
return globalObject.throwInvalidArguments("Expected a pointer", .{});
|
||||
}
|
||||
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
|
||||
const value = @as(*align(1) i32, @ptrFromInt(addr)).*;
|
||||
return JSValue.jsNumber(value);
|
||||
}
|
||||
pub fn intptr(
|
||||
globalObject: *JSGlobalObject,
|
||||
_: JSValue,
|
||||
arguments: []const JSValue,
|
||||
) bun.JSError!JSValue {
|
||||
if (arguments.len == 0 or !arguments[0].isNumber()) {
|
||||
return globalObject.throwInvalidArguments("Expected a pointer", .{});
|
||||
}
|
||||
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
|
||||
const value = @as(*align(1) i64, @ptrFromInt(addr)).*;
|
||||
return JSValue.jsNumber(value);
|
||||
}
|
||||
|
||||
pub fn @"f32"(
|
||||
globalObject: *JSGlobalObject,
|
||||
_: JSValue,
|
||||
arguments: []const JSValue,
|
||||
) bun.JSError!JSValue {
|
||||
if (arguments.len == 0 or !arguments[0].isNumber()) {
|
||||
return globalObject.throwInvalidArguments("Expected a pointer", .{});
|
||||
}
|
||||
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
|
||||
const value = @as(*align(1) f32, @ptrFromInt(addr)).*;
|
||||
return JSValue.jsNumber(value);
|
||||
}
|
||||
|
||||
pub fn @"f64"(
|
||||
globalObject: *JSGlobalObject,
|
||||
_: JSValue,
|
||||
arguments: []const JSValue,
|
||||
) bun.JSError!JSValue {
|
||||
if (arguments.len == 0 or !arguments[0].isNumber()) {
|
||||
return globalObject.throwInvalidArguments("Expected a pointer", .{});
|
||||
}
|
||||
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
|
||||
const value = @as(*align(1) f64, @ptrFromInt(addr)).*;
|
||||
return JSValue.jsNumber(value);
|
||||
}
|
||||
|
||||
pub fn @"i64"(
|
||||
globalObject: *JSGlobalObject,
|
||||
_: JSValue,
|
||||
arguments: []const JSValue,
|
||||
) bun.JSError!JSValue {
|
||||
if (arguments.len == 0 or !arguments[0].isNumber()) {
|
||||
return globalObject.throwInvalidArguments("Expected a pointer", .{});
|
||||
}
|
||||
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
|
||||
const value = @as(*align(1) i64, @ptrFromInt(addr)).*;
|
||||
return JSValue.fromInt64NoTruncate(globalObject, value);
|
||||
}
|
||||
|
||||
pub fn @"u64"(
|
||||
globalObject: *JSGlobalObject,
|
||||
_: JSValue,
|
||||
arguments: []const JSValue,
|
||||
) bun.JSError!JSValue {
|
||||
if (arguments.len == 0 or !arguments[0].isNumber()) {
|
||||
return globalObject.throwInvalidArguments("Expected a pointer", .{});
|
||||
}
|
||||
const addr = arguments[0].asPtrAddress() + if (arguments.len > 1) @as(usize, @intCast(arguments[1].to(i32))) else @as(usize, 0);
|
||||
const value = @as(*align(1) u64, @ptrFromInt(addr)).*;
|
||||
return JSValue.fromUInt64NoTruncate(globalObject, value);
|
||||
}
|
||||
|
||||
pub fn u8WithoutTypeChecks(
|
||||
_: *JSGlobalObject,
|
||||
_: *anyopaque,
|
||||
raw_addr: i64,
|
||||
offset: i32,
|
||||
) callconv(JSC.conv) JSValue {
|
||||
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
|
||||
const value = @as(*align(1) u8, @ptrFromInt(addr)).*;
|
||||
return JSValue.jsNumber(value);
|
||||
}
|
||||
pub fn u16WithoutTypeChecks(
|
||||
_: *JSGlobalObject,
|
||||
_: *anyopaque,
|
||||
raw_addr: i64,
|
||||
offset: i32,
|
||||
) callconv(JSC.conv) JSValue {
|
||||
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
|
||||
const value = @as(*align(1) u16, @ptrFromInt(addr)).*;
|
||||
return JSValue.jsNumber(value);
|
||||
}
|
||||
pub fn u32WithoutTypeChecks(
|
||||
_: *JSGlobalObject,
|
||||
_: *anyopaque,
|
||||
raw_addr: i64,
|
||||
offset: i32,
|
||||
) callconv(JSC.conv) JSValue {
|
||||
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
|
||||
const value = @as(*align(1) u32, @ptrFromInt(addr)).*;
|
||||
return JSValue.jsNumber(value);
|
||||
}
|
||||
pub fn ptrWithoutTypeChecks(
|
||||
_: *JSGlobalObject,
|
||||
_: *anyopaque,
|
||||
raw_addr: i64,
|
||||
offset: i32,
|
||||
) callconv(JSC.conv) JSValue {
|
||||
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
|
||||
const value = @as(*align(1) u64, @ptrFromInt(addr)).*;
|
||||
return JSValue.jsNumber(value);
|
||||
}
|
||||
pub fn i8WithoutTypeChecks(
|
||||
_: *JSGlobalObject,
|
||||
_: *anyopaque,
|
||||
raw_addr: i64,
|
||||
offset: i32,
|
||||
) callconv(JSC.conv) JSValue {
|
||||
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
|
||||
const value = @as(*align(1) i8, @ptrFromInt(addr)).*;
|
||||
return JSValue.jsNumber(value);
|
||||
}
|
||||
pub fn i16WithoutTypeChecks(
|
||||
_: *JSGlobalObject,
|
||||
_: *anyopaque,
|
||||
raw_addr: i64,
|
||||
offset: i32,
|
||||
) callconv(JSC.conv) JSValue {
|
||||
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
|
||||
const value = @as(*align(1) i16, @ptrFromInt(addr)).*;
|
||||
return JSValue.jsNumber(value);
|
||||
}
|
||||
pub fn i32WithoutTypeChecks(
|
||||
_: *JSGlobalObject,
|
||||
_: *anyopaque,
|
||||
raw_addr: i64,
|
||||
offset: i32,
|
||||
) callconv(JSC.conv) JSValue {
|
||||
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
|
||||
const value = @as(*align(1) i32, @ptrFromInt(addr)).*;
|
||||
return JSValue.jsNumber(value);
|
||||
}
|
||||
pub fn intptrWithoutTypeChecks(
|
||||
_: *JSGlobalObject,
|
||||
_: *anyopaque,
|
||||
raw_addr: i64,
|
||||
offset: i32,
|
||||
) callconv(JSC.conv) JSValue {
|
||||
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
|
||||
const value = @as(*align(1) i64, @ptrFromInt(addr)).*;
|
||||
return JSValue.jsNumber(value);
|
||||
}
|
||||
|
||||
pub fn f32WithoutTypeChecks(
|
||||
_: *JSGlobalObject,
|
||||
_: *anyopaque,
|
||||
raw_addr: i64,
|
||||
offset: i32,
|
||||
) callconv(JSC.conv) JSValue {
|
||||
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
|
||||
const value = @as(*align(1) f32, @ptrFromInt(addr)).*;
|
||||
return JSValue.jsNumber(value);
|
||||
}
|
||||
|
||||
pub fn f64WithoutTypeChecks(
|
||||
_: *JSGlobalObject,
|
||||
_: *anyopaque,
|
||||
raw_addr: i64,
|
||||
offset: i32,
|
||||
) callconv(JSC.conv) JSValue {
|
||||
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
|
||||
const value = @as(*align(1) f64, @ptrFromInt(addr)).*;
|
||||
return JSValue.jsNumber(value);
|
||||
}
|
||||
|
||||
pub fn u64WithoutTypeChecks(
|
||||
global: *JSGlobalObject,
|
||||
_: *anyopaque,
|
||||
raw_addr: i64,
|
||||
offset: i32,
|
||||
) callconv(JSC.conv) JSValue {
|
||||
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
|
||||
const value = @as(*align(1) u64, @ptrFromInt(addr)).*;
|
||||
return JSValue.fromUInt64NoTruncate(global, value);
|
||||
}
|
||||
|
||||
pub fn i64WithoutTypeChecks(
|
||||
global: *JSGlobalObject,
|
||||
_: *anyopaque,
|
||||
raw_addr: i64,
|
||||
offset: i32,
|
||||
) callconv(JSC.conv) JSValue {
|
||||
const addr = @as(usize, @intCast(raw_addr)) + @as(usize, @intCast(offset));
|
||||
const value = @as(*align(1) i64, @ptrFromInt(addr)).*;
|
||||
return JSValue.fromInt64NoTruncate(global, value);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn ptr(
|
||||
globalThis: *JSGlobalObject,
|
||||
_: JSValue,
|
||||
arguments: []const JSValue,
|
||||
) JSValue {
|
||||
return switch (arguments.len) {
|
||||
0 => ptr_(globalThis, JSValue.zero, null),
|
||||
1 => ptr_(globalThis, arguments[0], null),
|
||||
else => ptr_(globalThis, arguments[0], arguments[1]),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn ptrWithoutTypeChecks(
|
||||
_: *JSGlobalObject,
|
||||
_: *anyopaque,
|
||||
array: *JSC.JSUint8Array,
|
||||
) callconv(JSC.conv) JSValue {
|
||||
return JSValue.fromPtrAddress(@intFromPtr(array.ptr()));
|
||||
}
|
||||
|
||||
fn ptr_(
|
||||
globalThis: *JSGlobalObject,
|
||||
value: JSValue,
|
||||
byteOffset: ?JSValue,
|
||||
) JSValue {
|
||||
if (value == .zero) {
|
||||
return JSC.JSValue.jsNull();
|
||||
}
|
||||
|
||||
const array_buffer = value.asArrayBuffer(globalThis) orelse {
|
||||
return JSC.toInvalidArguments("Expected ArrayBufferView but received {s}", .{@tagName(value.jsType())}, globalThis);
|
||||
};
|
||||
|
||||
if (array_buffer.len == 0) {
|
||||
return JSC.toInvalidArguments("ArrayBufferView must have a length > 0. A pointer to empty memory doesn't work", .{}, globalThis);
|
||||
}
|
||||
|
||||
var addr: usize = @intFromPtr(array_buffer.ptr);
|
||||
// const Sizes = @import("../bindings/sizes.zig");
|
||||
// assert(addr == @intFromPtr(value.asEncoded().ptr) + Sizes.Bun_FFI_PointerOffsetToTypedArrayVector);
|
||||
|
||||
if (byteOffset) |off| {
|
||||
if (!off.isEmptyOrUndefinedOrNull()) {
|
||||
if (!off.isNumber()) {
|
||||
return JSC.toInvalidArguments("Expected number for byteOffset", .{}, globalThis);
|
||||
}
|
||||
}
|
||||
|
||||
const bytei64 = off.toInt64();
|
||||
if (bytei64 < 0) {
|
||||
addr -|= @as(usize, @intCast(bytei64 * -1));
|
||||
} else {
|
||||
addr += @as(usize, @intCast(bytei64));
|
||||
}
|
||||
|
||||
if (addr > @intFromPtr(array_buffer.ptr) + @as(usize, array_buffer.byte_len)) {
|
||||
return JSC.toInvalidArguments("byteOffset out of bounds", .{}, globalThis);
|
||||
}
|
||||
}
|
||||
|
||||
if (addr > max_addressable_memory) {
|
||||
return JSC.toInvalidArguments("Pointer is outside max addressible memory, which usually means a bug in your program.", .{}, globalThis);
|
||||
}
|
||||
|
||||
if (addr == 0) {
|
||||
return JSC.toInvalidArguments("Pointer must not be 0", .{}, globalThis);
|
||||
}
|
||||
|
||||
if (addr == 0xDEADBEEF or addr == 0xaaaaaaaa or addr == 0xAAAAAAAA) {
|
||||
return JSC.toInvalidArguments("ptr to invalid memory, that would segfault Bun :(", .{}, globalThis);
|
||||
}
|
||||
|
||||
if (comptime Environment.allow_assert) {
|
||||
assert(JSC.JSValue.fromPtrAddress(addr).asPtrAddress() == addr);
|
||||
}
|
||||
|
||||
return JSC.JSValue.fromPtrAddress(addr);
|
||||
}
|
||||
|
||||
const ValueOrError = union(enum) {
|
||||
err: JSValue,
|
||||
slice: []u8,
|
||||
};
|
||||
|
||||
pub fn getPtrSlice(globalThis: *JSGlobalObject, value: JSValue, byteOffset: ?JSValue, byteLength: ?JSValue) ValueOrError {
|
||||
if (!value.isNumber()) {
|
||||
return .{ .err = JSC.toInvalidArguments("ptr must be a number.", .{}, globalThis) };
|
||||
}
|
||||
|
||||
const num = value.asPtrAddress();
|
||||
if (num == 0) {
|
||||
return .{ .err = JSC.toInvalidArguments("ptr cannot be zero, that would segfault Bun :(", .{}, globalThis) };
|
||||
}
|
||||
|
||||
// if (!std.math.isFinite(num)) {
|
||||
// return .{ .err = JSC.toInvalidArguments("ptr must be a finite number.", .{}, globalThis) };
|
||||
// }
|
||||
|
||||
var addr = @as(usize, @bitCast(num));
|
||||
|
||||
if (byteOffset) |byte_off| {
|
||||
if (byte_off.isNumber()) {
|
||||
const off = byte_off.toInt64();
|
||||
if (off < 0) {
|
||||
addr -|= @as(usize, @intCast(off * -1));
|
||||
} else {
|
||||
addr +|= @as(usize, @intCast(off));
|
||||
}
|
||||
|
||||
if (addr == 0) {
|
||||
return .{ .err = JSC.toInvalidArguments("ptr cannot be zero, that would segfault Bun :(", .{}, globalThis) };
|
||||
}
|
||||
|
||||
if (!std.math.isFinite(byte_off.asNumber())) {
|
||||
return .{ .err = JSC.toInvalidArguments("ptr must be a finite number.", .{}, globalThis) };
|
||||
}
|
||||
} else if (!byte_off.isEmptyOrUndefinedOrNull()) {
|
||||
// do nothing
|
||||
} else {
|
||||
return .{ .err = JSC.toInvalidArguments("Expected number for byteOffset", .{}, globalThis) };
|
||||
}
|
||||
}
|
||||
|
||||
if (addr == 0xDEADBEEF or addr == 0xaaaaaaaa or addr == 0xAAAAAAAA) {
|
||||
return .{ .err = JSC.toInvalidArguments("ptr to invalid memory, that would segfault Bun :(", .{}, globalThis) };
|
||||
}
|
||||
|
||||
if (byteLength) |valueLength| {
|
||||
if (!valueLength.isEmptyOrUndefinedOrNull()) {
|
||||
if (!valueLength.isNumber()) {
|
||||
return .{ .err = JSC.toInvalidArguments("length must be a number.", .{}, globalThis) };
|
||||
}
|
||||
|
||||
if (valueLength.asNumber() == 0.0) {
|
||||
return .{ .err = JSC.toInvalidArguments("length must be > 0. This usually means a bug in your code.", .{}, globalThis) };
|
||||
}
|
||||
|
||||
const length_i = valueLength.toInt64();
|
||||
if (length_i < 0) {
|
||||
return .{ .err = JSC.toInvalidArguments("length must be > 0. This usually means a bug in your code.", .{}, globalThis) };
|
||||
}
|
||||
|
||||
if (length_i > max_addressable_memory) {
|
||||
return .{ .err = JSC.toInvalidArguments("length exceeds max addressable memory. This usually means a bug in your code.", .{}, globalThis) };
|
||||
}
|
||||
|
||||
const length = @as(usize, @intCast(length_i));
|
||||
return .{ .slice = @as([*]u8, @ptrFromInt(addr))[0..length] };
|
||||
}
|
||||
}
|
||||
|
||||
return .{ .slice = bun.span(@as([*:0]u8, @ptrFromInt(addr))) };
|
||||
}
|
||||
|
||||
fn getCPtr(value: JSValue) ?usize {
|
||||
// pointer to C function
|
||||
if (value.isNumber()) {
|
||||
const addr = value.asPtrAddress();
|
||||
if (addr > 0) return addr;
|
||||
} else if (value.isBigInt()) {
|
||||
const addr = @as(u64, @bitCast(value.toUInt64NoTruncate()));
|
||||
if (addr > 0) {
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn toArrayBuffer(
|
||||
globalThis: *JSGlobalObject,
|
||||
value: JSValue,
|
||||
byteOffset: ?JSValue,
|
||||
valueLength: ?JSValue,
|
||||
finalizationCtxOrPtr: ?JSValue,
|
||||
finalizationCallback: ?JSValue,
|
||||
) JSC.JSValue {
|
||||
switch (getPtrSlice(globalThis, value, byteOffset, valueLength)) {
|
||||
.err => |erro| {
|
||||
return erro;
|
||||
},
|
||||
.slice => |slice| {
|
||||
var callback: JSC.C.JSTypedArrayBytesDeallocator = null;
|
||||
var ctx: ?*anyopaque = null;
|
||||
if (finalizationCallback) |callback_value| {
|
||||
if (getCPtr(callback_value)) |callback_ptr| {
|
||||
callback = @as(JSC.C.JSTypedArrayBytesDeallocator, @ptrFromInt(callback_ptr));
|
||||
|
||||
if (finalizationCtxOrPtr) |ctx_value| {
|
||||
if (getCPtr(ctx_value)) |ctx_ptr| {
|
||||
ctx = @as(*anyopaque, @ptrFromInt(ctx_ptr));
|
||||
} else if (!ctx_value.isUndefinedOrNull()) {
|
||||
return JSC.toInvalidArguments("Expected user data to be a C pointer (number or BigInt)", .{}, globalThis);
|
||||
}
|
||||
}
|
||||
} else if (!callback_value.isEmptyOrUndefinedOrNull()) {
|
||||
return JSC.toInvalidArguments("Expected callback to be a C pointer (number or BigInt)", .{}, globalThis);
|
||||
}
|
||||
} else if (finalizationCtxOrPtr) |callback_value| {
|
||||
if (getCPtr(callback_value)) |callback_ptr| {
|
||||
callback = @as(JSC.C.JSTypedArrayBytesDeallocator, @ptrFromInt(callback_ptr));
|
||||
} else if (!callback_value.isEmptyOrUndefinedOrNull()) {
|
||||
return JSC.toInvalidArguments("Expected callback to be a C pointer (number or BigInt)", .{}, globalThis);
|
||||
}
|
||||
}
|
||||
|
||||
return JSC.ArrayBuffer.fromBytes(slice, JSC.JSValue.JSType.ArrayBuffer).toJSWithContext(globalThis, ctx, callback, null);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toBuffer(
|
||||
globalThis: *JSGlobalObject,
|
||||
value: JSValue,
|
||||
byteOffset: ?JSValue,
|
||||
valueLength: ?JSValue,
|
||||
finalizationCtxOrPtr: ?JSValue,
|
||||
finalizationCallback: ?JSValue,
|
||||
) JSC.JSValue {
|
||||
switch (getPtrSlice(globalThis, value, byteOffset, valueLength)) {
|
||||
.err => |err| {
|
||||
return err;
|
||||
},
|
||||
.slice => |slice| {
|
||||
var callback: JSC.C.JSTypedArrayBytesDeallocator = null;
|
||||
var ctx: ?*anyopaque = null;
|
||||
if (finalizationCallback) |callback_value| {
|
||||
if (getCPtr(callback_value)) |callback_ptr| {
|
||||
callback = @as(JSC.C.JSTypedArrayBytesDeallocator, @ptrFromInt(callback_ptr));
|
||||
|
||||
if (finalizationCtxOrPtr) |ctx_value| {
|
||||
if (getCPtr(ctx_value)) |ctx_ptr| {
|
||||
ctx = @as(*anyopaque, @ptrFromInt(ctx_ptr));
|
||||
} else if (!ctx_value.isEmptyOrUndefinedOrNull()) {
|
||||
return JSC.toInvalidArguments("Expected user data to be a C pointer (number or BigInt)", .{}, globalThis);
|
||||
}
|
||||
}
|
||||
} else if (!callback_value.isEmptyOrUndefinedOrNull()) {
|
||||
return JSC.toInvalidArguments("Expected callback to be a C pointer (number or BigInt)", .{}, globalThis);
|
||||
}
|
||||
} else if (finalizationCtxOrPtr) |callback_value| {
|
||||
if (getCPtr(callback_value)) |callback_ptr| {
|
||||
callback = @as(JSC.C.JSTypedArrayBytesDeallocator, @ptrFromInt(callback_ptr));
|
||||
} else if (!callback_value.isEmptyOrUndefinedOrNull()) {
|
||||
return JSC.toInvalidArguments("Expected callback to be a C pointer (number or BigInt)", .{}, globalThis);
|
||||
}
|
||||
}
|
||||
|
||||
if (callback != null or ctx != null) {
|
||||
return JSC.JSValue.createBufferWithCtx(globalThis, slice, ctx, callback);
|
||||
}
|
||||
|
||||
return JSC.JSValue.createBuffer(globalThis, slice, null);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toCStringBuffer(
|
||||
globalThis: *JSGlobalObject,
|
||||
value: JSValue,
|
||||
byteOffset: ?JSValue,
|
||||
valueLength: ?JSValue,
|
||||
) JSC.JSValue {
|
||||
switch (getPtrSlice(globalThis, value, byteOffset, valueLength)) {
|
||||
.err => |err| {
|
||||
return err;
|
||||
},
|
||||
.slice => |slice| {
|
||||
return JSC.JSValue.createBuffer(globalThis, slice, null);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getter(
|
||||
globalObject: *JSC.JSGlobalObject,
|
||||
_: *JSC.JSObject,
|
||||
) JSC.JSValue {
|
||||
return FFIObject.toJS(globalObject);
|
||||
}
|
||||
|
||||
const fields = .{
|
||||
.viewSource = JSC.wrapStaticMethod(
|
||||
JSC.FFI,
|
||||
"print",
|
||||
false,
|
||||
),
|
||||
.dlopen = JSC.wrapStaticMethod(JSC.FFI, "open", false),
|
||||
.callback = JSC.wrapStaticMethod(JSC.FFI, "callback", false),
|
||||
.linkSymbols = JSC.wrapStaticMethod(JSC.FFI, "linkSymbols", false),
|
||||
.toBuffer = JSC.wrapStaticMethod(@This(), "toBuffer", false),
|
||||
.toArrayBuffer = JSC.wrapStaticMethod(@This(), "toArrayBuffer", false),
|
||||
.closeCallback = JSC.wrapStaticMethod(JSC.FFI, "closeCallback", false),
|
||||
.CString = JSC.wrapStaticMethod(Bun.FFIObject, "newCString", false),
|
||||
};
|
||||
const max_addressable_memory = std.math.maxInt(u56);
|
||||
|
||||
const JSGlobalObject = JSC.JSGlobalObject;
|
||||
const JSObject = JSC.JSObject;
|
||||
const JSValue = JSC.JSValue;
|
||||
const JSC = bun.JSC;
|
||||
const bun = @import("root").bun;
|
||||
const FFIObject = @This();
|
||||
const Bun = JSC.API.Bun;
|
||||
|
||||
const Environment = bun.Environment;
|
||||
const std = @import("std");
|
||||
const assert = bun.assert;
|
||||
const ZigString = JSC.ZigString;
|
||||
144
src/bun.js/api/HashObject.zig
Normal file
144
src/bun.js/api/HashObject.zig
Normal file
@@ -0,0 +1,144 @@
|
||||
pub const wyhash = hashWrap(std.hash.Wyhash);
|
||||
pub const adler32 = hashWrap(std.hash.Adler32);
|
||||
pub const crc32 = hashWrap(std.hash.Crc32);
|
||||
pub const cityHash32 = hashWrap(std.hash.CityHash32);
|
||||
pub const cityHash64 = hashWrap(std.hash.CityHash64);
|
||||
pub const xxHash32 = hashWrap(struct {
|
||||
pub fn hash(seed: u32, bytes: []const u8) u32 {
|
||||
// sidestep .hash taking in anytype breaking ArgTuple
|
||||
// downstream by forcing a type signature on the input
|
||||
return std.hash.XxHash32.hash(seed, bytes);
|
||||
}
|
||||
});
|
||||
pub const xxHash64 = hashWrap(struct {
|
||||
pub fn hash(seed: u32, bytes: []const u8) u64 {
|
||||
// sidestep .hash taking in anytype breaking ArgTuple
|
||||
// downstream by forcing a type signature on the input
|
||||
return std.hash.XxHash64.hash(seed, bytes);
|
||||
}
|
||||
});
|
||||
pub const xxHash3 = hashWrap(struct {
|
||||
pub fn hash(seed: u32, bytes: []const u8) u64 {
|
||||
// sidestep .hash taking in anytype breaking ArgTuple
|
||||
// downstream by forcing a type signature on the input
|
||||
return std.hash.XxHash3.hash(seed, bytes);
|
||||
}
|
||||
});
|
||||
pub const murmur32v2 = hashWrap(std.hash.murmur.Murmur2_32);
|
||||
pub const murmur32v3 = hashWrap(std.hash.murmur.Murmur3_32);
|
||||
pub const murmur64v2 = hashWrap(std.hash.murmur.Murmur2_64);
|
||||
|
||||
pub fn create(globalThis: *JSC.JSGlobalObject) JSC.JSValue {
|
||||
const function = JSC.createCallback(globalThis, ZigString.static("hash"), 1, wyhash);
|
||||
const fns = comptime .{
|
||||
"wyhash",
|
||||
"adler32",
|
||||
"crc32",
|
||||
"cityHash32",
|
||||
"cityHash64",
|
||||
"xxHash32",
|
||||
"xxHash64",
|
||||
"xxHash3",
|
||||
"murmur32v2",
|
||||
"murmur32v3",
|
||||
"murmur64v2",
|
||||
};
|
||||
inline for (fns) |name| {
|
||||
const value = JSC.createCallback(
|
||||
globalThis,
|
||||
ZigString.static(name),
|
||||
1,
|
||||
@field(HashObject, name),
|
||||
);
|
||||
function.put(globalThis, comptime ZigString.static(name), value);
|
||||
}
|
||||
|
||||
return function;
|
||||
}
|
||||
|
||||
fn hashWrap(comptime Hasher_: anytype) JSC.JSHostZigFunction {
|
||||
return struct {
|
||||
const Hasher = Hasher_;
|
||||
pub fn hash(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
|
||||
const arguments = callframe.arguments_old(2).slice();
|
||||
var args = JSC.Node.ArgumentsSlice.init(globalThis.bunVM(), arguments);
|
||||
defer args.deinit();
|
||||
|
||||
var input: []const u8 = "";
|
||||
var input_slice = ZigString.Slice.empty;
|
||||
defer input_slice.deinit();
|
||||
if (args.nextEat()) |arg| {
|
||||
if (arg.as(JSC.WebCore.Blob)) |blob| {
|
||||
// TODO: files
|
||||
input = blob.sharedView();
|
||||
} else {
|
||||
switch (arg.jsTypeLoose()) {
|
||||
.ArrayBuffer,
|
||||
.Int8Array,
|
||||
.Uint8Array,
|
||||
.Uint8ClampedArray,
|
||||
.Int16Array,
|
||||
.Uint16Array,
|
||||
.Int32Array,
|
||||
.Uint32Array,
|
||||
.Float16Array,
|
||||
.Float32Array,
|
||||
.Float64Array,
|
||||
.BigInt64Array,
|
||||
.BigUint64Array,
|
||||
.DataView,
|
||||
=> {
|
||||
var array_buffer = arg.asArrayBuffer(globalThis) orelse {
|
||||
return globalThis.throwInvalidArguments("ArrayBuffer conversion error", .{});
|
||||
};
|
||||
input = array_buffer.byteSlice();
|
||||
},
|
||||
else => {
|
||||
input_slice = try arg.toSlice(globalThis, bun.default_allocator);
|
||||
input = input_slice.slice();
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// std.hash has inconsistent interfaces
|
||||
//
|
||||
const Function = if (@hasDecl(Hasher, "hashWithSeed")) Hasher.hashWithSeed else Hasher.hash;
|
||||
var function_args: std.meta.ArgsTuple(@TypeOf(Function)) = undefined;
|
||||
if (comptime std.meta.fields(std.meta.ArgsTuple(@TypeOf(Function))).len == 1) {
|
||||
return JSC.JSValue.jsNumber(Function(input));
|
||||
} else {
|
||||
var seed: u64 = 0;
|
||||
if (args.nextEat()) |arg| {
|
||||
if (arg.isNumber() or arg.isBigInt()) {
|
||||
seed = arg.toUInt64NoTruncate();
|
||||
}
|
||||
}
|
||||
if (comptime bun.trait.isNumber(@TypeOf(function_args[0]))) {
|
||||
function_args[0] = @as(@TypeOf(function_args[0]), @truncate(seed));
|
||||
function_args[1] = input;
|
||||
} else {
|
||||
function_args[0] = input;
|
||||
function_args[1] = @as(@TypeOf(function_args[1]), @truncate(seed));
|
||||
}
|
||||
|
||||
const value = @call(.auto, Function, function_args);
|
||||
|
||||
if (@TypeOf(value) == u32) {
|
||||
return JSC.JSValue.jsNumber(@as(u32, @bitCast(value)));
|
||||
}
|
||||
return JSC.JSValue.fromUInt64NoTruncate(globalThis, value);
|
||||
}
|
||||
}
|
||||
}.hash;
|
||||
}
|
||||
|
||||
const HashObject = @This();
|
||||
|
||||
const JSC = bun.JSC;
|
||||
const JSValue = JSC.JSValue;
|
||||
const JSGlobalObject = JSC.JSGlobalObject;
|
||||
const JSObject = JSC.JSObject;
|
||||
const std = @import("std");
|
||||
const bun = @import("root").bun;
|
||||
const ZigString = JSC.ZigString;
|
||||
72
src/bun.js/api/TOMLObject.zig
Normal file
72
src/bun.js/api/TOMLObject.zig
Normal file
@@ -0,0 +1,72 @@
|
||||
pub fn create(globalThis: *JSC.JSGlobalObject) JSC.JSValue {
|
||||
const object = JSValue.createEmptyObject(globalThis, 1);
|
||||
object.put(
|
||||
globalThis,
|
||||
ZigString.static("parse"),
|
||||
JSC.createCallback(
|
||||
globalThis,
|
||||
ZigString.static("parse"),
|
||||
1,
|
||||
parse,
|
||||
),
|
||||
);
|
||||
|
||||
return object;
|
||||
}
|
||||
|
||||
pub fn parse(
|
||||
globalThis: *JSC.JSGlobalObject,
|
||||
callframe: *JSC.CallFrame,
|
||||
) bun.JSError!JSC.JSValue {
|
||||
var arena = bun.ArenaAllocator.init(globalThis.allocator());
|
||||
const allocator = arena.allocator();
|
||||
defer arena.deinit();
|
||||
var log = logger.Log.init(default_allocator);
|
||||
const arguments = callframe.arguments_old(1).slice();
|
||||
if (arguments.len == 0 or arguments[0].isEmptyOrUndefinedOrNull()) {
|
||||
return globalThis.throwInvalidArguments("Expected a string to parse", .{});
|
||||
}
|
||||
|
||||
var input_slice = try arguments[0].toSlice(globalThis, bun.default_allocator);
|
||||
defer input_slice.deinit();
|
||||
var source = logger.Source.initPathString("input.toml", input_slice.slice());
|
||||
const parse_result = TOMLParser.parse(&source, &log, allocator, false) catch {
|
||||
return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to parse toml"));
|
||||
};
|
||||
|
||||
// for now...
|
||||
const buffer_writer = js_printer.BufferWriter.init(allocator) catch {
|
||||
return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to print toml"));
|
||||
};
|
||||
var writer = js_printer.BufferPrinter.init(buffer_writer);
|
||||
_ = js_printer.printJSON(
|
||||
*js_printer.BufferPrinter,
|
||||
&writer,
|
||||
parse_result,
|
||||
&source,
|
||||
.{
|
||||
.mangled_props = null,
|
||||
},
|
||||
) catch {
|
||||
return globalThis.throwValue(log.toJS(globalThis, default_allocator, "Failed to print toml"));
|
||||
};
|
||||
|
||||
const slice = writer.ctx.buffer.slice();
|
||||
var out = bun.String.fromUTF8(slice);
|
||||
defer out.deref();
|
||||
|
||||
return out.toJSByParseJSON(globalThis);
|
||||
}
|
||||
|
||||
const TOMLObject = @This();
|
||||
const JSC = bun.JSC;
|
||||
const JSValue = JSC.JSValue;
|
||||
const JSGlobalObject = JSC.JSGlobalObject;
|
||||
const JSObject = JSC.JSObject;
|
||||
const std = @import("std");
|
||||
const ZigString = JSC.ZigString;
|
||||
const logger = bun.logger;
|
||||
const bun = @import("root").bun;
|
||||
const js_printer = bun.js_printer;
|
||||
const default_allocator = bun.default_allocator;
|
||||
const TOMLParser = @import("../../toml/toml_parser.zig").TOML;
|
||||
76
src/bun.js/api/UnsafeObject.zig
Normal file
76
src/bun.js/api/UnsafeObject.zig
Normal file
@@ -0,0 +1,76 @@
|
||||
pub fn create(globalThis: *JSC.JSGlobalObject) JSC.JSValue {
|
||||
const object = JSValue.createEmptyObject(globalThis, 3);
|
||||
const fields = comptime .{
|
||||
.gcAggressionLevel = gcAggressionLevel,
|
||||
.arrayBufferToString = arrayBufferToString,
|
||||
.mimallocDump = dump_mimalloc,
|
||||
};
|
||||
inline for (comptime std.meta.fieldNames(@TypeOf(fields))) |name| {
|
||||
object.put(
|
||||
globalThis,
|
||||
comptime ZigString.static(name),
|
||||
JSC.createCallback(globalThis, comptime ZigString.static(name), 1, comptime @field(fields, name)),
|
||||
);
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
pub fn gcAggressionLevel(
|
||||
globalThis: *JSC.JSGlobalObject,
|
||||
callframe: *JSC.CallFrame,
|
||||
) bun.JSError!JSC.JSValue {
|
||||
const ret = JSValue.jsNumber(@as(i32, @intFromEnum(globalThis.bunVM().aggressive_garbage_collection)));
|
||||
const value = callframe.arguments_old(1).ptr[0];
|
||||
|
||||
if (!value.isEmptyOrUndefinedOrNull()) {
|
||||
switch (value.coerce(i32, globalThis)) {
|
||||
1 => globalThis.bunVM().aggressive_garbage_collection = .mild,
|
||||
2 => globalThis.bunVM().aggressive_garbage_collection = .aggressive,
|
||||
0 => globalThis.bunVM().aggressive_garbage_collection = .none,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
pub fn arrayBufferToString(
|
||||
globalThis: *JSC.JSGlobalObject,
|
||||
callframe: *JSC.CallFrame,
|
||||
) bun.JSError!JSC.JSValue {
|
||||
const args = callframe.arguments_old(2).slice();
|
||||
if (args.len < 1 or !args[0].isCell() or !args[0].jsType().isTypedArrayOrArrayBuffer()) {
|
||||
return globalThis.throwInvalidArguments("Expected an ArrayBuffer", .{});
|
||||
}
|
||||
|
||||
const array_buffer = JSC.ArrayBuffer.fromTypedArray(globalThis, args[0]);
|
||||
switch (array_buffer.typed_array_type) {
|
||||
.Uint16Array, .Int16Array => {
|
||||
var zig_str = ZigString.init("");
|
||||
zig_str._unsafe_ptr_do_not_use = @as([*]const u8, @ptrCast(@alignCast(array_buffer.ptr)));
|
||||
zig_str.len = array_buffer.len;
|
||||
zig_str.markUTF16();
|
||||
return zig_str.toJS(globalThis);
|
||||
},
|
||||
else => {
|
||||
return ZigString.init(array_buffer.slice()).toJS(globalThis);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
extern fn dump_zone_malloc_stats() void;
|
||||
|
||||
fn dump_mimalloc(globalObject: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSC.JSValue {
|
||||
globalObject.bunVM().arena.dumpStats();
|
||||
if (bun.heap_breakdown.enabled) {
|
||||
dump_zone_malloc_stats();
|
||||
}
|
||||
return .undefined;
|
||||
}
|
||||
|
||||
const JSC = bun.JSC;
|
||||
const JSValue = JSC.JSValue;
|
||||
const JSGlobalObject = JSC.JSGlobalObject;
|
||||
const JSObject = JSC.JSObject;
|
||||
const std = @import("std");
|
||||
const bun = @import("root").bun;
|
||||
const ZigString = JSC.ZigString;
|
||||
32
src/bun.js/api/crypto.zig
Normal file
32
src/bun.js/api/crypto.zig
Normal file
@@ -0,0 +1,32 @@
|
||||
pub fn createCryptoError(globalThis: *JSC.JSGlobalObject, err_code: u32) JSValue {
|
||||
return bun.BoringSSL.ERR_toJS(globalThis, err_code);
|
||||
}
|
||||
|
||||
pub const PasswordObject = @import("./crypto/PasswordObject.zig").PasswordObject;
|
||||
pub const JSPasswordObject = @import("./crypto/PasswordObject.zig").JSPasswordObject;
|
||||
|
||||
pub const CryptoHasher = @import("./crypto/CryptoHasher.zig").CryptoHasher;
|
||||
pub const MD4 = @import("./crypto/CryptoHasher.zig").MD4;
|
||||
pub const MD5 = @import("./crypto/CryptoHasher.zig").MD5;
|
||||
pub const SHA1 = @import("./crypto/CryptoHasher.zig").SHA1;
|
||||
pub const SHA224 = @import("./crypto/CryptoHasher.zig").SHA224;
|
||||
pub const SHA256 = @import("./crypto/CryptoHasher.zig").SHA256;
|
||||
pub const SHA384 = @import("./crypto/CryptoHasher.zig").SHA384;
|
||||
pub const SHA512 = @import("./crypto/CryptoHasher.zig").SHA512;
|
||||
pub const SHA512_256 = @import("./crypto/CryptoHasher.zig").SHA512_256;
|
||||
pub const HMAC = @import("./crypto/HMAC.zig");
|
||||
pub const EVP = @import("./crypto/EVP.zig");
|
||||
|
||||
comptime {
|
||||
CryptoHasher.Extern.@"export"();
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
const bun = @import("root").bun;
|
||||
const string = bun.string;
|
||||
const strings = bun.strings;
|
||||
const MutableString = bun.MutableString;
|
||||
const stringZ = bun.stringZ;
|
||||
const JSC = bun.JSC;
|
||||
const JSValue = JSC.JSValue;
|
||||
const JSGlobalObject = JSC.JSGlobalObject;
|
||||
898
src/bun.js/api/crypto/CryptoHasher.zig
Normal file
898
src/bun.js/api/crypto/CryptoHasher.zig
Normal file
@@ -0,0 +1,898 @@
|
||||
pub const CryptoHasher = union(enum) {
|
||||
// HMAC_CTX contains 3 EVP_CTX, so let's store it as a pointer.
|
||||
hmac: ?*HMAC,
|
||||
|
||||
evp: EVP,
|
||||
zig: CryptoHasherZig,
|
||||
|
||||
const Digest = EVP.Digest;
|
||||
|
||||
pub usingnamespace JSC.Codegen.JSCryptoHasher;
|
||||
usingnamespace bun.New(@This());
|
||||
|
||||
// For using only CryptoHasherZig in c++
|
||||
pub const Extern = struct {
|
||||
fn getByName(global: *JSGlobalObject, name_bytes: [*:0]const u8, name_len: usize) callconv(.C) ?*CryptoHasher {
|
||||
const name = name_bytes[0..name_len];
|
||||
|
||||
if (CryptoHasherZig.init(name)) |inner| {
|
||||
return CryptoHasher.new(.{
|
||||
.zig = inner,
|
||||
});
|
||||
}
|
||||
|
||||
const algorithm = EVP.Algorithm.map.get(name) orelse {
|
||||
return null;
|
||||
};
|
||||
|
||||
switch (algorithm) {
|
||||
.ripemd160,
|
||||
.blake2b256,
|
||||
.blake2b512,
|
||||
|
||||
.@"sha512-224",
|
||||
=> {
|
||||
if (algorithm.md()) |md| {
|
||||
return CryptoHasher.new(.{
|
||||
.evp = EVP.init(algorithm, md, global.bunVM().rareData().boringEngine()),
|
||||
});
|
||||
}
|
||||
},
|
||||
else => {
|
||||
return null;
|
||||
},
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn getFromOther(global: *JSGlobalObject, other_handle: *CryptoHasher) callconv(.C) ?*CryptoHasher {
|
||||
switch (other_handle.*) {
|
||||
.zig => |other| {
|
||||
const hasher = CryptoHasher.new(.{
|
||||
.zig = other.copy(),
|
||||
});
|
||||
return hasher;
|
||||
},
|
||||
.evp => |other| {
|
||||
return CryptoHasher.new(.{
|
||||
.evp = other.copy(global.bunVM().rareData().boringEngine()) catch {
|
||||
return null;
|
||||
},
|
||||
});
|
||||
},
|
||||
else => {
|
||||
return null;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn destroy(handle: *CryptoHasher) callconv(.C) void {
|
||||
handle.finalize();
|
||||
}
|
||||
|
||||
fn update(handle: *CryptoHasher, input_bytes: [*]const u8, input_len: usize) callconv(.C) bool {
|
||||
const input = input_bytes[0..input_len];
|
||||
|
||||
switch (handle.*) {
|
||||
.zig => {
|
||||
handle.zig.update(input);
|
||||
return true;
|
||||
},
|
||||
.evp => {
|
||||
handle.evp.update(input);
|
||||
return true;
|
||||
},
|
||||
else => {
|
||||
return false;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn digest(handle: *CryptoHasher, global: *JSGlobalObject, buf: [*]u8, buf_len: usize) callconv(.C) u32 {
|
||||
const digest_buf = buf[0..buf_len];
|
||||
switch (handle.*) {
|
||||
.zig => {
|
||||
const res = handle.zig.finalWithLen(digest_buf, buf_len);
|
||||
return @intCast(res.len);
|
||||
},
|
||||
.evp => {
|
||||
const res = handle.evp.final(global.bunVM().rareData().boringEngine(), digest_buf);
|
||||
return @intCast(res.len);
|
||||
},
|
||||
else => {
|
||||
return 0;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn getDigestSize(handle: *CryptoHasher) callconv(.C) u32 {
|
||||
return switch (handle.*) {
|
||||
.zig => |inner| inner.digest_length,
|
||||
.evp => |inner| inner.size(),
|
||||
else => 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn @"export"() void {
|
||||
@export(&CryptoHasher.Extern.getByName, .{ .name = "Bun__CryptoHasherExtern__getByName" });
|
||||
@export(&CryptoHasher.Extern.getFromOther, .{ .name = "Bun__CryptoHasherExtern__getFromOther" });
|
||||
@export(&CryptoHasher.Extern.destroy, .{ .name = "Bun__CryptoHasherExtern__destroy" });
|
||||
@export(&CryptoHasher.Extern.update, .{ .name = "Bun__CryptoHasherExtern__update" });
|
||||
@export(&CryptoHasher.Extern.digest, .{ .name = "Bun__CryptoHasherExtern__digest" });
|
||||
@export(&CryptoHasher.Extern.getDigestSize, .{ .name = "Bun__CryptoHasherExtern__getDigestSize" });
|
||||
}
|
||||
};
|
||||
|
||||
pub const digest = JSC.wrapInstanceMethod(CryptoHasher, "digest_", false);
|
||||
pub const hash = JSC.wrapStaticMethod(CryptoHasher, "hash_", false);
|
||||
|
||||
fn throwHmacConsumed(globalThis: *JSC.JSGlobalObject) bun.JSError {
|
||||
return globalThis.throw("HMAC has been consumed and is no longer usable", .{});
|
||||
}
|
||||
|
||||
pub fn getByteLength(this: *CryptoHasher, globalThis: *JSC.JSGlobalObject) JSC.JSValue {
|
||||
return JSC.JSValue.jsNumber(switch (this.*) {
|
||||
.evp => |*inner| inner.size(),
|
||||
.hmac => |inner| if (inner) |hmac| hmac.size() else {
|
||||
throwHmacConsumed(globalThis) catch return .zero;
|
||||
},
|
||||
.zig => |*inner| inner.digest_length,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn getAlgorithm(this: *CryptoHasher, globalObject: *JSC.JSGlobalObject) JSC.JSValue {
|
||||
return switch (this.*) {
|
||||
inline .evp, .zig => |*inner| ZigString.fromUTF8(bun.asByteSlice(@tagName(inner.algorithm))).toJS(globalObject),
|
||||
.hmac => |inner| if (inner) |hmac| ZigString.fromUTF8(bun.asByteSlice(@tagName(hmac.algorithm))).toJS(globalObject) else {
|
||||
throwHmacConsumed(globalObject) catch return .zero;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getAlgorithms(
|
||||
globalThis_: *JSC.JSGlobalObject,
|
||||
_: JSValue,
|
||||
_: JSValue,
|
||||
) JSC.JSValue {
|
||||
return bun.String.toJSArray(globalThis_, &EVP.Algorithm.names.values);
|
||||
}
|
||||
|
||||
fn hashToEncoding(globalThis: *JSGlobalObject, evp: *EVP, input: JSC.Node.BlobOrStringOrBuffer, encoding: JSC.Node.Encoding) bun.JSError!JSC.JSValue {
|
||||
var output_digest_buf: Digest = undefined;
|
||||
defer input.deinit();
|
||||
|
||||
if (input == .blob and input.blob.isBunFile()) {
|
||||
return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{});
|
||||
}
|
||||
|
||||
const len = evp.hash(globalThis.bunVM().rareData().boringEngine(), input.slice(), &output_digest_buf) orelse {
|
||||
const err = BoringSSL.ERR_get_error();
|
||||
const instance = createCryptoError(globalThis, err);
|
||||
BoringSSL.ERR_clear_error();
|
||||
return globalThis.throwValue(instance);
|
||||
};
|
||||
return encoding.encodeWithMaxSize(globalThis, BoringSSL.EVP_MAX_MD_SIZE, output_digest_buf[0..len]);
|
||||
}
|
||||
|
||||
fn hashToBytes(globalThis: *JSGlobalObject, evp: *EVP, input: JSC.Node.BlobOrStringOrBuffer, output: ?JSC.ArrayBuffer) bun.JSError!JSC.JSValue {
|
||||
var output_digest_buf: Digest = undefined;
|
||||
var output_digest_slice: []u8 = &output_digest_buf;
|
||||
defer input.deinit();
|
||||
|
||||
if (input == .blob and input.blob.isBunFile()) {
|
||||
return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{});
|
||||
}
|
||||
|
||||
if (output) |output_buf| {
|
||||
const size = evp.size();
|
||||
var bytes = output_buf.byteSlice();
|
||||
if (bytes.len < size) {
|
||||
return globalThis.throwInvalidArguments("TypedArray must be at least {d} bytes", .{size});
|
||||
}
|
||||
output_digest_slice = bytes[0..size];
|
||||
}
|
||||
|
||||
const len = evp.hash(globalThis.bunVM().rareData().boringEngine(), input.slice(), output_digest_slice) orelse {
|
||||
const err = BoringSSL.ERR_get_error();
|
||||
const instance = createCryptoError(globalThis, err);
|
||||
BoringSSL.ERR_clear_error();
|
||||
return globalThis.throwValue(instance);
|
||||
};
|
||||
|
||||
if (output) |output_buf| {
|
||||
return output_buf.value;
|
||||
} else {
|
||||
// Clone to GC-managed memory
|
||||
return JSC.ArrayBuffer.createBuffer(globalThis, output_digest_slice[0..len]);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hash_(
|
||||
globalThis: *JSGlobalObject,
|
||||
algorithm: ZigString,
|
||||
input: JSC.Node.BlobOrStringOrBuffer,
|
||||
output: ?JSC.Node.StringOrBuffer,
|
||||
) bun.JSError!JSC.JSValue {
|
||||
var evp = EVP.byName(algorithm, globalThis) orelse return try CryptoHasherZig.hashByName(globalThis, algorithm, input, output) orelse {
|
||||
return globalThis.throwInvalidArguments("Unsupported algorithm \"{any}\"", .{algorithm});
|
||||
};
|
||||
defer evp.deinit();
|
||||
|
||||
if (output) |string_or_buffer| {
|
||||
switch (string_or_buffer) {
|
||||
inline else => |*str| {
|
||||
defer str.deinit();
|
||||
const encoding = JSC.Node.Encoding.from(str.slice()) orelse {
|
||||
return globalThis.ERR_INVALID_ARG_VALUE("Unknown encoding: {s}", .{str.slice()}).throw();
|
||||
};
|
||||
|
||||
return hashToEncoding(globalThis, &evp, input, encoding);
|
||||
},
|
||||
.buffer => |buffer| {
|
||||
return hashToBytes(globalThis, &evp, input, buffer.buffer);
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return hashToBytes(globalThis, &evp, input, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Bun.CryptoHasher(algorithm, hmacKey?: string | Buffer)
|
||||
pub fn constructor(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!*CryptoHasher {
|
||||
const arguments = callframe.arguments_old(2);
|
||||
if (arguments.len == 0) {
|
||||
return globalThis.throwInvalidArguments("Expected an algorithm name as an argument", .{});
|
||||
}
|
||||
|
||||
const algorithm_name = arguments.ptr[0];
|
||||
if (algorithm_name.isEmptyOrUndefinedOrNull() or !algorithm_name.isString()) {
|
||||
return globalThis.throwInvalidArguments("algorithm must be a string", .{});
|
||||
}
|
||||
|
||||
const algorithm = try algorithm_name.getZigString(globalThis);
|
||||
|
||||
if (algorithm.len == 0) {
|
||||
return globalThis.throwInvalidArguments("Invalid algorithm name", .{});
|
||||
}
|
||||
|
||||
const hmac_value = arguments.ptr[1];
|
||||
var hmac_key: ?JSC.Node.StringOrBuffer = null;
|
||||
defer {
|
||||
if (hmac_key) |*key| {
|
||||
key.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
if (!hmac_value.isEmptyOrUndefinedOrNull()) {
|
||||
hmac_key = try JSC.Node.StringOrBuffer.fromJS(globalThis, bun.default_allocator, hmac_value) orelse {
|
||||
return globalThis.throwInvalidArguments("key must be a string or buffer", .{});
|
||||
};
|
||||
}
|
||||
|
||||
return CryptoHasher.new(brk: {
|
||||
if (hmac_key) |*key| {
|
||||
const chosen_algorithm = try algorithm_name.toEnumFromMap(globalThis, "algorithm", EVP.Algorithm, EVP.Algorithm.map);
|
||||
if (chosen_algorithm == .ripemd160) {
|
||||
// crashes at runtime.
|
||||
return globalThis.throw("ripemd160 is not supported", .{});
|
||||
}
|
||||
|
||||
break :brk .{
|
||||
.hmac = HMAC.init(chosen_algorithm, key.slice()) orelse {
|
||||
if (!globalThis.hasException()) {
|
||||
const err = BoringSSL.ERR_get_error();
|
||||
if (err != 0) {
|
||||
const instance = createCryptoError(globalThis, err);
|
||||
BoringSSL.ERR_clear_error();
|
||||
return globalThis.throwValue(instance);
|
||||
} else {
|
||||
return globalThis.throwTODO("HMAC is not supported for this algorithm yet");
|
||||
}
|
||||
}
|
||||
return error.JSError;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
break :brk .{
|
||||
.evp = EVP.byName(algorithm, globalThis) orelse return CryptoHasherZig.constructor(algorithm) orelse {
|
||||
return globalThis.throwInvalidArguments("Unsupported algorithm {any}", .{algorithm});
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
pub fn getter(
|
||||
globalObject: *JSC.JSGlobalObject,
|
||||
_: *JSC.JSObject,
|
||||
) JSC.JSValue {
|
||||
return CryptoHasher.getConstructor(globalObject);
|
||||
}
|
||||
|
||||
pub fn update(this: *CryptoHasher, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
|
||||
const thisValue = callframe.this();
|
||||
const arguments = callframe.arguments_old(2);
|
||||
const input = arguments.ptr[0];
|
||||
if (input.isEmptyOrUndefinedOrNull()) {
|
||||
return globalThis.throwInvalidArguments("expected blob, string or buffer", .{});
|
||||
}
|
||||
const encoding = arguments.ptr[1];
|
||||
const buffer = try JSC.Node.BlobOrStringOrBuffer.fromJSWithEncodingValue(globalThis, globalThis.bunVM().allocator, input, encoding) orelse {
|
||||
if (!globalThis.hasException()) return globalThis.throwInvalidArguments("expected blob, string or buffer", .{});
|
||||
return error.JSError;
|
||||
};
|
||||
defer buffer.deinit();
|
||||
if (buffer == .blob and buffer.blob.isBunFile()) {
|
||||
return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{});
|
||||
}
|
||||
|
||||
switch (this.*) {
|
||||
.evp => |*inner| {
|
||||
inner.update(buffer.slice());
|
||||
const err = BoringSSL.ERR_get_error();
|
||||
if (err != 0) {
|
||||
const instance = createCryptoError(globalThis, err);
|
||||
BoringSSL.ERR_clear_error();
|
||||
return globalThis.throwValue(instance);
|
||||
}
|
||||
},
|
||||
.hmac => |inner| {
|
||||
const hmac = inner orelse {
|
||||
return throwHmacConsumed(globalThis);
|
||||
};
|
||||
|
||||
hmac.update(buffer.slice());
|
||||
const err = BoringSSL.ERR_get_error();
|
||||
if (err != 0) {
|
||||
const instance = createCryptoError(globalThis, err);
|
||||
BoringSSL.ERR_clear_error();
|
||||
return globalThis.throwValue(instance);
|
||||
}
|
||||
},
|
||||
.zig => |*inner| {
|
||||
inner.update(buffer.slice());
|
||||
return thisValue;
|
||||
},
|
||||
}
|
||||
|
||||
return thisValue;
|
||||
}
|
||||
|
||||
pub fn copy(
|
||||
this: *CryptoHasher,
|
||||
globalObject: *JSC.JSGlobalObject,
|
||||
_: *JSC.CallFrame,
|
||||
) bun.JSError!JSC.JSValue {
|
||||
var new: CryptoHasher = undefined;
|
||||
switch (this.*) {
|
||||
.evp => |*inner| {
|
||||
new = .{ .evp = inner.copy(globalObject.bunVM().rareData().boringEngine()) catch bun.outOfMemory() };
|
||||
},
|
||||
.hmac => |inner| {
|
||||
const hmac = inner orelse {
|
||||
return throwHmacConsumed(globalObject);
|
||||
};
|
||||
new = .{
|
||||
.hmac = hmac.copy() catch {
|
||||
const err = createCryptoError(globalObject, BoringSSL.ERR_get_error());
|
||||
BoringSSL.ERR_clear_error();
|
||||
return globalObject.throwValue(err);
|
||||
},
|
||||
};
|
||||
},
|
||||
.zig => |*inner| {
|
||||
new = .{ .zig = inner.copy() };
|
||||
},
|
||||
}
|
||||
return CryptoHasher.new(new).toJS(globalObject);
|
||||
}
|
||||
|
||||
pub fn digest_(this: *CryptoHasher, globalThis: *JSGlobalObject, output: ?JSC.Node.StringOrBuffer) bun.JSError!JSC.JSValue {
|
||||
if (output) |string_or_buffer| {
|
||||
switch (string_or_buffer) {
|
||||
inline else => |*str| {
|
||||
defer str.deinit();
|
||||
const encoding = JSC.Node.Encoding.from(str.slice()) orelse {
|
||||
return globalThis.ERR_INVALID_ARG_VALUE("Unknown encoding: {s}", .{str.slice()}).throw();
|
||||
};
|
||||
|
||||
return this.digestToEncoding(globalThis, encoding);
|
||||
},
|
||||
.buffer => |buffer| {
|
||||
return this.digestToBytes(
|
||||
globalThis,
|
||||
buffer.buffer,
|
||||
);
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return this.digestToBytes(globalThis, null);
|
||||
}
|
||||
}
|
||||
|
||||
fn digestToBytes(this: *CryptoHasher, globalThis: *JSGlobalObject, output: ?JSC.ArrayBuffer) bun.JSError!JSC.JSValue {
|
||||
var output_digest_buf: EVP.Digest = undefined;
|
||||
var output_digest_slice: []u8 = &output_digest_buf;
|
||||
if (output) |output_buf| {
|
||||
var bytes = output_buf.byteSlice();
|
||||
if (bytes.len < output_digest_buf.len) {
|
||||
return globalThis.throwInvalidArguments(comptime std.fmt.comptimePrint("TypedArray must be at least {d} bytes", .{output_digest_buf.len}), .{});
|
||||
}
|
||||
output_digest_slice = bytes[0..bytes.len];
|
||||
} else {
|
||||
output_digest_buf = std.mem.zeroes(EVP.Digest);
|
||||
}
|
||||
|
||||
const result = this.final(globalThis, output_digest_slice) catch return .zero;
|
||||
if (globalThis.hasException()) {
|
||||
return error.JSError;
|
||||
}
|
||||
|
||||
if (output) |output_buf| {
|
||||
return output_buf.value;
|
||||
} else {
|
||||
// Clone to GC-managed memory
|
||||
return JSC.ArrayBuffer.createBuffer(globalThis, result);
|
||||
}
|
||||
}
|
||||
|
||||
fn digestToEncoding(this: *CryptoHasher, globalThis: *JSGlobalObject, encoding: JSC.Node.Encoding) bun.JSError!JSC.JSValue {
|
||||
var output_digest_buf: EVP.Digest = std.mem.zeroes(EVP.Digest);
|
||||
const output_digest_slice: []u8 = &output_digest_buf;
|
||||
const out = this.final(globalThis, output_digest_slice) catch return .zero;
|
||||
if (globalThis.hasException()) {
|
||||
return error.JSError;
|
||||
}
|
||||
return encoding.encodeWithMaxSize(globalThis, BoringSSL.EVP_MAX_MD_SIZE, out);
|
||||
}
|
||||
|
||||
fn final(this: *CryptoHasher, globalThis: *JSGlobalObject, output_digest_slice: []u8) bun.JSError![]u8 {
|
||||
return switch (this.*) {
|
||||
.hmac => |inner| brk: {
|
||||
const hmac: *HMAC = inner orelse {
|
||||
return throwHmacConsumed(globalThis);
|
||||
};
|
||||
this.hmac = null;
|
||||
defer hmac.deinit();
|
||||
break :brk hmac.final(output_digest_slice);
|
||||
},
|
||||
.evp => |*inner| inner.final(globalThis.bunVM().rareData().boringEngine(), output_digest_slice),
|
||||
.zig => |*inner| inner.final(output_digest_slice),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn finalize(this: *CryptoHasher) void {
|
||||
switch (this.*) {
|
||||
.evp => |*inner| {
|
||||
// https://github.com/oven-sh/bun/issues/3250
|
||||
inner.deinit();
|
||||
},
|
||||
.zig => |*inner| {
|
||||
inner.deinit();
|
||||
},
|
||||
.hmac => |inner| {
|
||||
if (inner) |hmac| {
|
||||
hmac.deinit();
|
||||
}
|
||||
},
|
||||
}
|
||||
this.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
const CryptoHasherZig = struct {
|
||||
algorithm: EVP.Algorithm,
|
||||
state: *anyopaque,
|
||||
digest_length: u8,
|
||||
|
||||
const algo_map = [_]struct { string, type }{
|
||||
.{ "sha3-224", std.crypto.hash.sha3.Sha3_224 },
|
||||
.{ "sha3-256", std.crypto.hash.sha3.Sha3_256 },
|
||||
.{ "sha3-384", std.crypto.hash.sha3.Sha3_384 },
|
||||
.{ "sha3-512", std.crypto.hash.sha3.Sha3_512 },
|
||||
.{ "shake128", std.crypto.hash.sha3.Shake128 },
|
||||
.{ "shake256", std.crypto.hash.sha3.Shake256 },
|
||||
};
|
||||
|
||||
inline fn digestLength(Algorithm: type) comptime_int {
|
||||
return switch (Algorithm) {
|
||||
std.crypto.hash.sha3.Shake128 => 16,
|
||||
std.crypto.hash.sha3.Shake256 => 32,
|
||||
else => Algorithm.digest_length,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn hashByName(globalThis: *JSGlobalObject, algorithm: ZigString, input: JSC.Node.BlobOrStringOrBuffer, output: ?JSC.Node.StringOrBuffer) bun.JSError!?JSC.JSValue {
|
||||
inline for (algo_map) |item| {
|
||||
if (bun.strings.eqlComptime(algorithm.slice(), item[0])) {
|
||||
return try hashByNameInner(globalThis, item[1], input, output);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn hashByNameInner(globalThis: *JSGlobalObject, comptime Algorithm: type, input: JSC.Node.BlobOrStringOrBuffer, output: ?JSC.Node.StringOrBuffer) bun.JSError!JSC.JSValue {
|
||||
if (output) |string_or_buffer| {
|
||||
switch (string_or_buffer) {
|
||||
inline else => |*str| {
|
||||
defer str.deinit();
|
||||
const encoding = JSC.Node.Encoding.from(str.slice()) orelse {
|
||||
return globalThis.ERR_INVALID_ARG_VALUE("Unknown encoding: {s}", .{str.slice()}).throw();
|
||||
};
|
||||
|
||||
if (encoding == .buffer) {
|
||||
return hashByNameInnerToBytes(globalThis, Algorithm, input, null);
|
||||
}
|
||||
|
||||
return hashByNameInnerToString(globalThis, Algorithm, input, encoding);
|
||||
},
|
||||
.buffer => |buffer| {
|
||||
return hashByNameInnerToBytes(globalThis, Algorithm, input, buffer.buffer);
|
||||
},
|
||||
}
|
||||
}
|
||||
return hashByNameInnerToBytes(globalThis, Algorithm, input, null);
|
||||
}
|
||||
|
||||
fn hashByNameInnerToString(globalThis: *JSGlobalObject, comptime Algorithm: type, input: JSC.Node.BlobOrStringOrBuffer, encoding: JSC.Node.Encoding) bun.JSError!JSC.JSValue {
|
||||
defer input.deinit();
|
||||
|
||||
if (input == .blob and input.blob.isBunFile()) {
|
||||
return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{});
|
||||
}
|
||||
|
||||
var h = Algorithm.init(.{});
|
||||
h.update(input.slice());
|
||||
|
||||
var out: [digestLength(Algorithm)]u8 = undefined;
|
||||
h.final(&out);
|
||||
|
||||
return encoding.encodeWithSize(globalThis, digestLength(Algorithm), &out);
|
||||
}
|
||||
|
||||
fn hashByNameInnerToBytes(globalThis: *JSGlobalObject, comptime Algorithm: type, input: JSC.Node.BlobOrStringOrBuffer, output: ?JSC.ArrayBuffer) bun.JSError!JSC.JSValue {
|
||||
defer input.deinit();
|
||||
|
||||
if (input == .blob and input.blob.isBunFile()) {
|
||||
return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{});
|
||||
}
|
||||
|
||||
var h = Algorithm.init(.{});
|
||||
const digest_length_comptime = digestLength(Algorithm);
|
||||
|
||||
if (output) |output_buf| {
|
||||
if (output_buf.byteSlice().len < digest_length_comptime) {
|
||||
return globalThis.throwInvalidArguments("TypedArray must be at least {d} bytes", .{digest_length_comptime});
|
||||
}
|
||||
}
|
||||
|
||||
h.update(input.slice());
|
||||
|
||||
if (output) |output_buf| {
|
||||
h.final(output_buf.slice()[0..digest_length_comptime]);
|
||||
return output_buf.value;
|
||||
} else {
|
||||
var out: [digestLength(Algorithm)]u8 = undefined;
|
||||
h.final(&out);
|
||||
// Clone to GC-managed memory
|
||||
return JSC.ArrayBuffer.createBuffer(globalThis, &out);
|
||||
}
|
||||
}
|
||||
|
||||
fn constructor(algorithm: ZigString) ?*CryptoHasher {
|
||||
inline for (algo_map) |item| {
|
||||
if (bun.strings.eqlComptime(algorithm.slice(), item[0])) {
|
||||
return CryptoHasher.new(.{ .zig = .{
|
||||
.algorithm = @field(EVP.Algorithm, item[0]),
|
||||
.state = bun.new(item[1], item[1].init(.{})),
|
||||
.digest_length = digestLength(item[1]),
|
||||
} });
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn init(algorithm: []const u8) ?CryptoHasherZig {
|
||||
inline for (algo_map) |item| {
|
||||
const name, const T = item;
|
||||
if (bun.strings.eqlComptime(algorithm, name)) {
|
||||
const handle: CryptoHasherZig = .{
|
||||
.algorithm = @field(EVP.Algorithm, name),
|
||||
.state = bun.new(T, T.init(.{})),
|
||||
.digest_length = digestLength(T),
|
||||
};
|
||||
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn update(self: *CryptoHasherZig, bytes: []const u8) void {
|
||||
inline for (algo_map) |item| {
|
||||
if (self.algorithm == @field(EVP.Algorithm, item[0])) {
|
||||
return item[1].update(@ptrCast(@alignCast(self.state)), bytes);
|
||||
}
|
||||
}
|
||||
@panic("unreachable");
|
||||
}
|
||||
|
||||
fn copy(self: *const CryptoHasherZig) CryptoHasherZig {
|
||||
inline for (algo_map) |item| {
|
||||
if (self.algorithm == @field(EVP.Algorithm, item[0])) {
|
||||
return .{
|
||||
.algorithm = self.algorithm,
|
||||
.state = bun.dupe(item[1], @ptrCast(@alignCast(self.state))),
|
||||
.digest_length = self.digest_length,
|
||||
};
|
||||
}
|
||||
}
|
||||
@panic("unreachable");
|
||||
}
|
||||
|
||||
fn finalWithLen(self: *CryptoHasherZig, output_digest_slice: []u8, res_len: usize) []u8 {
|
||||
inline for (algo_map) |pair| {
|
||||
const name, const T = pair;
|
||||
if (self.algorithm == @field(EVP.Algorithm, name)) {
|
||||
T.final(@ptrCast(@alignCast(self.state)), @ptrCast(output_digest_slice));
|
||||
const reset: *T = @ptrCast(@alignCast(self.state));
|
||||
reset.* = T.init(.{});
|
||||
return output_digest_slice[0..res_len];
|
||||
}
|
||||
}
|
||||
@panic("unreachable");
|
||||
}
|
||||
|
||||
fn final(self: *CryptoHasherZig, output_digest_slice: []u8) []u8 {
|
||||
return self.finalWithLen(output_digest_slice, self.digest_length);
|
||||
}
|
||||
|
||||
fn deinit(self: *CryptoHasherZig) void {
|
||||
inline for (algo_map) |item| {
|
||||
if (self.algorithm == @field(EVP.Algorithm, item[0])) {
|
||||
return bun.destroy(@as(*item[1], @ptrCast(@alignCast(self.state))));
|
||||
}
|
||||
}
|
||||
@panic("unreachable");
|
||||
}
|
||||
};
|
||||
|
||||
fn StaticCryptoHasher(comptime Hasher: type, comptime name: [:0]const u8) type {
|
||||
return struct {
|
||||
hashing: Hasher = Hasher{},
|
||||
digested: bool = false,
|
||||
|
||||
const ThisHasher = @This();
|
||||
|
||||
pub usingnamespace @field(JSC.Codegen, "JS" ++ name);
|
||||
|
||||
pub const digest = JSC.wrapInstanceMethod(ThisHasher, "digest_", false);
|
||||
pub const hash = JSC.wrapStaticMethod(ThisHasher, "hash_", false);
|
||||
|
||||
pub fn getByteLength(
|
||||
_: *@This(),
|
||||
_: *JSC.JSGlobalObject,
|
||||
) JSC.JSValue {
|
||||
return JSC.JSValue.jsNumber(@as(u16, Hasher.digest));
|
||||
}
|
||||
|
||||
pub fn getByteLengthStatic(
|
||||
_: *JSC.JSGlobalObject,
|
||||
_: JSValue,
|
||||
_: JSValue,
|
||||
) JSC.JSValue {
|
||||
return JSC.JSValue.jsNumber(@as(u16, Hasher.digest));
|
||||
}
|
||||
|
||||
fn hashToEncoding(globalThis: *JSGlobalObject, input: JSC.Node.BlobOrStringOrBuffer, encoding: JSC.Node.Encoding) bun.JSError!JSC.JSValue {
|
||||
var output_digest_buf: Hasher.Digest = undefined;
|
||||
|
||||
if (input == .blob and input.blob.isBunFile()) {
|
||||
return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{});
|
||||
}
|
||||
|
||||
if (comptime @typeInfo(@TypeOf(Hasher.hash)).@"fn".params.len == 3) {
|
||||
Hasher.hash(input.slice(), &output_digest_buf, JSC.VirtualMachine.get().rareData().boringEngine());
|
||||
} else {
|
||||
Hasher.hash(input.slice(), &output_digest_buf);
|
||||
}
|
||||
|
||||
return encoding.encodeWithSize(globalThis, Hasher.digest, &output_digest_buf);
|
||||
}
|
||||
|
||||
fn hashToBytes(globalThis: *JSGlobalObject, input: JSC.Node.BlobOrStringOrBuffer, output: ?JSC.ArrayBuffer) bun.JSError!JSC.JSValue {
|
||||
var output_digest_buf: Hasher.Digest = undefined;
|
||||
var output_digest_slice: *Hasher.Digest = &output_digest_buf;
|
||||
if (output) |output_buf| {
|
||||
var bytes = output_buf.byteSlice();
|
||||
if (bytes.len < Hasher.digest) {
|
||||
return globalThis.throwInvalidArguments(comptime std.fmt.comptimePrint("TypedArray must be at least {d} bytes", .{Hasher.digest}), .{});
|
||||
}
|
||||
output_digest_slice = bytes[0..Hasher.digest];
|
||||
}
|
||||
|
||||
if (comptime @typeInfo(@TypeOf(Hasher.hash)).@"fn".params.len == 3) {
|
||||
Hasher.hash(input.slice(), output_digest_slice, JSC.VirtualMachine.get().rareData().boringEngine());
|
||||
} else {
|
||||
Hasher.hash(input.slice(), output_digest_slice);
|
||||
}
|
||||
|
||||
if (output) |output_buf| {
|
||||
return output_buf.value;
|
||||
} else {
|
||||
var array_buffer_out = JSC.ArrayBuffer.fromBytes(bun.default_allocator.dupe(u8, output_digest_slice) catch unreachable, .Uint8Array);
|
||||
return array_buffer_out.toJSUnchecked(globalThis, null);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hash_(
|
||||
globalThis: *JSGlobalObject,
|
||||
input: JSC.Node.BlobOrStringOrBuffer,
|
||||
output: ?JSC.Node.StringOrBuffer,
|
||||
) bun.JSError!JSC.JSValue {
|
||||
defer input.deinit();
|
||||
|
||||
if (input == .blob and input.blob.isBunFile()) {
|
||||
return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{});
|
||||
}
|
||||
|
||||
if (output) |string_or_buffer| {
|
||||
switch (string_or_buffer) {
|
||||
inline else => |*str| {
|
||||
defer str.deinit();
|
||||
const encoding = JSC.Node.Encoding.from(str.slice()) orelse {
|
||||
return globalThis.ERR_INVALID_ARG_VALUE("Unknown encoding: {s}", .{str.slice()}).throw();
|
||||
};
|
||||
|
||||
return hashToEncoding(globalThis, input, encoding);
|
||||
},
|
||||
.buffer => |buffer| {
|
||||
return hashToBytes(globalThis, input, buffer.buffer);
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return hashToBytes(globalThis, input, null);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn constructor(_: *JSC.JSGlobalObject, _: *JSC.CallFrame) bun.JSError!*@This() {
|
||||
const this = try bun.default_allocator.create(@This());
|
||||
this.* = .{ .hashing = Hasher.init() };
|
||||
return this;
|
||||
}
|
||||
|
||||
pub fn getter(
|
||||
globalObject: *JSC.JSGlobalObject,
|
||||
_: *JSC.JSObject,
|
||||
) JSC.JSValue {
|
||||
return ThisHasher.getConstructor(globalObject);
|
||||
}
|
||||
|
||||
pub fn update(this: *@This(), globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
|
||||
if (this.digested) {
|
||||
return globalThis.ERR_INVALID_STATE(name ++ " hasher already digested, create a new instance to update", .{}).throw();
|
||||
}
|
||||
const thisValue = callframe.this();
|
||||
const input = callframe.argument(0);
|
||||
const buffer = try JSC.Node.BlobOrStringOrBuffer.fromJS(globalThis, globalThis.bunVM().allocator, input) orelse {
|
||||
return globalThis.throwInvalidArguments("expected blob or string or buffer", .{});
|
||||
};
|
||||
defer buffer.deinit();
|
||||
|
||||
if (buffer == .blob and buffer.blob.isBunFile()) {
|
||||
return globalThis.throw("Bun.file() is not supported here yet (it needs an async version)", .{});
|
||||
}
|
||||
this.hashing.update(buffer.slice());
|
||||
return thisValue;
|
||||
}
|
||||
|
||||
pub fn digest_(
|
||||
this: *@This(),
|
||||
globalThis: *JSGlobalObject,
|
||||
output: ?JSC.Node.StringOrBuffer,
|
||||
) bun.JSError!JSC.JSValue {
|
||||
if (this.digested) {
|
||||
return globalThis.ERR_INVALID_STATE(name ++ " hasher already digested, create a new instance to digest again", .{}).throw();
|
||||
}
|
||||
if (output) |*string_or_buffer| {
|
||||
switch (string_or_buffer.*) {
|
||||
inline else => |*str| {
|
||||
defer str.deinit();
|
||||
const encoding = JSC.Node.Encoding.from(str.slice()) orelse {
|
||||
return globalThis.ERR_INVALID_ARG_VALUE("Unknown encoding: {s}", .{str.slice()}).throw();
|
||||
};
|
||||
|
||||
return this.digestToEncoding(globalThis, encoding);
|
||||
},
|
||||
.buffer => |*buffer| {
|
||||
return this.digestToBytes(
|
||||
globalThis,
|
||||
buffer.buffer,
|
||||
);
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return this.digestToBytes(globalThis, null);
|
||||
}
|
||||
}
|
||||
|
||||
fn digestToBytes(this: *@This(), globalThis: *JSGlobalObject, output: ?JSC.ArrayBuffer) bun.JSError!JSC.JSValue {
|
||||
var output_digest_buf: Hasher.Digest = undefined;
|
||||
var output_digest_slice: *Hasher.Digest = &output_digest_buf;
|
||||
if (output) |output_buf| {
|
||||
var bytes = output_buf.byteSlice();
|
||||
if (bytes.len < Hasher.digest) {
|
||||
return globalThis.throwInvalidArguments(comptime std.fmt.comptimePrint("TypedArray must be at least {d} bytes", .{Hasher.digest}), .{});
|
||||
}
|
||||
output_digest_slice = bytes[0..Hasher.digest];
|
||||
} else {
|
||||
output_digest_buf = std.mem.zeroes(Hasher.Digest);
|
||||
}
|
||||
|
||||
this.hashing.final(output_digest_slice);
|
||||
this.digested = true;
|
||||
|
||||
if (output) |output_buf| {
|
||||
return output_buf.value;
|
||||
} else {
|
||||
var array_buffer_out = JSC.ArrayBuffer.fromBytes(bun.default_allocator.dupe(u8, &output_digest_buf) catch unreachable, .Uint8Array);
|
||||
return array_buffer_out.toJSUnchecked(globalThis, null);
|
||||
}
|
||||
}
|
||||
|
||||
fn digestToEncoding(this: *@This(), globalThis: *JSGlobalObject, encoding: JSC.Node.Encoding) JSC.JSValue {
|
||||
var output_digest_buf: Hasher.Digest = comptime brk: {
|
||||
var bytes: Hasher.Digest = undefined;
|
||||
var i: usize = 0;
|
||||
while (i < Hasher.digest) {
|
||||
bytes[i] = 0;
|
||||
i += 1;
|
||||
}
|
||||
break :brk bytes;
|
||||
};
|
||||
|
||||
const output_digest_slice: *Hasher.Digest = &output_digest_buf;
|
||||
|
||||
this.hashing.final(output_digest_slice);
|
||||
this.digested = true;
|
||||
|
||||
return encoding.encodeWithSize(globalThis, Hasher.digest, output_digest_slice);
|
||||
}
|
||||
|
||||
pub fn finalize(this: *@This()) void {
|
||||
VirtualMachine.get().allocator.destroy(this);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const MD4 = StaticCryptoHasher(Hashers.MD4, "MD4");
|
||||
pub const MD5 = StaticCryptoHasher(Hashers.MD5, "MD5");
|
||||
pub const SHA1 = StaticCryptoHasher(Hashers.SHA1, "SHA1");
|
||||
pub const SHA224 = StaticCryptoHasher(Hashers.SHA224, "SHA224");
|
||||
pub const SHA256 = StaticCryptoHasher(Hashers.SHA256, "SHA256");
|
||||
pub const SHA384 = StaticCryptoHasher(Hashers.SHA384, "SHA384");
|
||||
pub const SHA512 = StaticCryptoHasher(Hashers.SHA512, "SHA512");
|
||||
pub const SHA512_256 = StaticCryptoHasher(Hashers.SHA512_256, "SHA512_256");
|
||||
const Crypto = JSC.API.Bun.Crypto;
|
||||
const Hashers = @import("../../../sha.zig");
|
||||
|
||||
const std = @import("std");
|
||||
const bun = @import("root").bun;
|
||||
const string = bun.string;
|
||||
const strings = bun.strings;
|
||||
const MutableString = bun.MutableString;
|
||||
const stringZ = bun.stringZ;
|
||||
const default_allocator = bun.default_allocator;
|
||||
const JSC = bun.JSC;
|
||||
const Async = bun.Async;
|
||||
const ZigString = JSC.ZigString;
|
||||
const JSValue = JSC.JSValue;
|
||||
const JSGlobalObject = JSC.JSGlobalObject;
|
||||
const CallFrame = JSC.CallFrame;
|
||||
const assert = bun.assert;
|
||||
const HMAC = Crypto.HMAC;
|
||||
const EVP = Crypto.EVP;
|
||||
const BoringSSL = bun.BoringSSL.c;
|
||||
const createCryptoError = Crypto.createCryptoError;
|
||||
const VirtualMachine = JSC.VirtualMachine;
|
||||
221
src/bun.js/api/crypto/EVP.zig
Normal file
221
src/bun.js/api/crypto/EVP.zig
Normal file
@@ -0,0 +1,221 @@
|
||||
ctx: BoringSSL.EVP_MD_CTX = undefined,
|
||||
md: *const BoringSSL.EVP_MD = undefined,
|
||||
algorithm: Algorithm,
|
||||
|
||||
// we do this to avoid asking BoringSSL what the digest name is
|
||||
// because that API is confusing
|
||||
pub const Algorithm = enum {
|
||||
// @"DSA-SHA",
|
||||
// @"DSA-SHA1",
|
||||
// @"MD5-SHA1",
|
||||
// @"RSA-MD5",
|
||||
// @"RSA-RIPEMD160",
|
||||
// @"RSA-SHA1",
|
||||
// @"RSA-SHA1-2",
|
||||
// @"RSA-SHA224",
|
||||
// @"RSA-SHA256",
|
||||
// @"RSA-SHA384",
|
||||
// @"RSA-SHA512",
|
||||
// @"ecdsa-with-SHA1",
|
||||
blake2b256,
|
||||
blake2b512,
|
||||
md4,
|
||||
md5,
|
||||
ripemd160,
|
||||
sha1,
|
||||
sha224,
|
||||
sha256,
|
||||
sha384,
|
||||
sha512,
|
||||
@"sha512-224",
|
||||
@"sha512-256",
|
||||
|
||||
@"sha3-224",
|
||||
@"sha3-256",
|
||||
@"sha3-384",
|
||||
@"sha3-512",
|
||||
shake128,
|
||||
shake256,
|
||||
|
||||
pub fn md(this: Algorithm) ?*const BoringSSL.EVP_MD {
|
||||
return switch (this) {
|
||||
.blake2b256 => BoringSSL.EVP_blake2b256(),
|
||||
.blake2b512 => BoringSSL.EVP_blake2b512(),
|
||||
.md4 => BoringSSL.EVP_md4(),
|
||||
.md5 => BoringSSL.EVP_md5(),
|
||||
.ripemd160 => BoringSSL.EVP_ripemd160(),
|
||||
.sha1 => BoringSSL.EVP_sha1(),
|
||||
.sha224 => BoringSSL.EVP_sha224(),
|
||||
.sha256 => BoringSSL.EVP_sha256(),
|
||||
.sha384 => BoringSSL.EVP_sha384(),
|
||||
.sha512 => BoringSSL.EVP_sha512(),
|
||||
.@"sha512-224" => BoringSSL.EVP_sha512_224(),
|
||||
.@"sha512-256" => BoringSSL.EVP_sha512_256(),
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
pub const names: std.EnumArray(Algorithm, bun.String) = brk: {
|
||||
var all = std.EnumArray(Algorithm, bun.String).initUndefined();
|
||||
var iter = all.iterator();
|
||||
while (iter.next()) |entry| {
|
||||
entry.value.* = bun.String.init(@tagName(entry.key));
|
||||
}
|
||||
break :brk all;
|
||||
};
|
||||
|
||||
pub const map = bun.ComptimeStringMap(Algorithm, .{
|
||||
.{ "blake2b256", .blake2b256 },
|
||||
.{ "blake2b512", .blake2b512 },
|
||||
.{ "ripemd160", .ripemd160 },
|
||||
.{ "rmd160", .ripemd160 },
|
||||
.{ "md4", .md4 },
|
||||
.{ "md5", .md5 },
|
||||
.{ "sha1", .sha1 },
|
||||
.{ "sha128", .sha1 },
|
||||
.{ "sha224", .sha224 },
|
||||
.{ "sha256", .sha256 },
|
||||
.{ "sha384", .sha384 },
|
||||
.{ "sha512", .sha512 },
|
||||
.{ "sha-1", .sha1 },
|
||||
.{ "sha-224", .sha224 },
|
||||
.{ "sha-256", .sha256 },
|
||||
.{ "sha-384", .sha384 },
|
||||
.{ "sha-512", .sha512 },
|
||||
.{ "sha-512/224", .@"sha512-224" },
|
||||
.{ "sha-512_224", .@"sha512-224" },
|
||||
.{ "sha-512224", .@"sha512-224" },
|
||||
.{ "sha512-224", .@"sha512-224" },
|
||||
.{ "sha-512/256", .@"sha512-256" },
|
||||
.{ "sha-512_256", .@"sha512-256" },
|
||||
.{ "sha-512256", .@"sha512-256" },
|
||||
.{ "sha512-256", .@"sha512-256" },
|
||||
.{ "sha384", .sha384 },
|
||||
.{ "sha3-224", .@"sha3-224" },
|
||||
.{ "sha3-256", .@"sha3-256" },
|
||||
.{ "sha3-384", .@"sha3-384" },
|
||||
.{ "sha3-512", .@"sha3-512" },
|
||||
.{ "shake128", .shake128 },
|
||||
.{ "shake256", .shake256 },
|
||||
// .{ "md5-sha1", .@"MD5-SHA1" },
|
||||
// .{ "dsa-sha", .@"DSA-SHA" },
|
||||
// .{ "dsa-sha1", .@"DSA-SHA1" },
|
||||
// .{ "ecdsa-with-sha1", .@"ecdsa-with-SHA1" },
|
||||
// .{ "rsa-md5", .@"RSA-MD5" },
|
||||
// .{ "rsa-sha1", .@"RSA-SHA1" },
|
||||
// .{ "rsa-sha1-2", .@"RSA-SHA1-2" },
|
||||
// .{ "rsa-sha224", .@"RSA-SHA224" },
|
||||
// .{ "rsa-sha256", .@"RSA-SHA256" },
|
||||
// .{ "rsa-sha384", .@"RSA-SHA384" },
|
||||
// .{ "rsa-sha512", .@"RSA-SHA512" },
|
||||
// .{ "rsa-ripemd160", .@"RSA-RIPEMD160" },
|
||||
});
|
||||
};
|
||||
|
||||
pub fn init(algorithm: Algorithm, md: *const BoringSSL.EVP_MD, engine: *BoringSSL.ENGINE) EVP {
|
||||
bun.BoringSSL.load();
|
||||
|
||||
var ctx: BoringSSL.EVP_MD_CTX = undefined;
|
||||
BoringSSL.EVP_MD_CTX_init(&ctx);
|
||||
_ = BoringSSL.EVP_DigestInit_ex(&ctx, md, engine);
|
||||
return .{
|
||||
.ctx = ctx,
|
||||
.md = md,
|
||||
.algorithm = algorithm,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn reset(this: *EVP, engine: *BoringSSL.ENGINE) void {
|
||||
BoringSSL.ERR_clear_error();
|
||||
_ = BoringSSL.EVP_DigestInit_ex(&this.ctx, this.md, engine);
|
||||
}
|
||||
|
||||
pub fn hash(this: *EVP, engine: *BoringSSL.ENGINE, input: []const u8, output: []u8) ?u32 {
|
||||
BoringSSL.ERR_clear_error();
|
||||
var outsize: c_uint = @min(@as(u16, @truncate(output.len)), this.size());
|
||||
if (BoringSSL.EVP_Digest(input.ptr, input.len, output.ptr, &outsize, this.md, engine) != 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return outsize;
|
||||
}
|
||||
|
||||
pub fn final(this: *EVP, engine: *BoringSSL.ENGINE, output: []u8) []u8 {
|
||||
BoringSSL.ERR_clear_error();
|
||||
var outsize: u32 = @min(@as(u16, @truncate(output.len)), this.size());
|
||||
if (BoringSSL.EVP_DigestFinal_ex(
|
||||
&this.ctx,
|
||||
output.ptr,
|
||||
&outsize,
|
||||
) != 1) {
|
||||
return "";
|
||||
}
|
||||
|
||||
this.reset(engine);
|
||||
|
||||
return output[0..outsize];
|
||||
}
|
||||
|
||||
pub fn update(this: *EVP, input: []const u8) void {
|
||||
BoringSSL.ERR_clear_error();
|
||||
_ = BoringSSL.EVP_DigestUpdate(&this.ctx, input.ptr, input.len);
|
||||
}
|
||||
|
||||
pub fn size(this: *const EVP) u16 {
|
||||
return @as(u16, @truncate(BoringSSL.EVP_MD_CTX_size(&this.ctx)));
|
||||
}
|
||||
|
||||
pub fn copy(this: *const EVP, engine: *BoringSSL.ENGINE) error{OutOfMemory}!EVP {
|
||||
BoringSSL.ERR_clear_error();
|
||||
var new = init(this.algorithm, this.md, engine);
|
||||
if (BoringSSL.EVP_MD_CTX_copy_ex(&new.ctx, &this.ctx) == 0) {
|
||||
return error.OutOfMemory;
|
||||
}
|
||||
return new;
|
||||
}
|
||||
|
||||
pub fn byNameAndEngine(engine: *BoringSSL.ENGINE, name: []const u8) ?EVP {
|
||||
if (Algorithm.map.getWithEql(name, strings.eqlCaseInsensitiveASCIIIgnoreLength)) |algorithm| {
|
||||
if (algorithm.md()) |md| {
|
||||
return EVP.init(algorithm, md, engine);
|
||||
}
|
||||
|
||||
if (BoringSSL.EVP_get_digestbyname(@tagName(algorithm))) |md| {
|
||||
return EVP.init(algorithm, md, engine);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn byName(name: ZigString, global: *JSC.JSGlobalObject) ?EVP {
|
||||
var name_str = name.toSlice(global.allocator());
|
||||
defer name_str.deinit();
|
||||
return byNameAndEngine(global.bunVM().rareData().boringEngine(), name_str.slice());
|
||||
}
|
||||
|
||||
pub fn deinit(this: *EVP) void {
|
||||
// https://github.com/oven-sh/bun/issues/3250
|
||||
_ = BoringSSL.EVP_MD_CTX_cleanup(&this.ctx);
|
||||
}
|
||||
|
||||
pub const Digest = [BoringSSL.EVP_MAX_MD_SIZE]u8;
|
||||
pub const PBKDF2 = @import("./PBKDF2.zig");
|
||||
pub const pbkdf2 = PBKDF2.pbkdf2;
|
||||
|
||||
const std = @import("std");
|
||||
const bun = @import("root").bun;
|
||||
const string = bun.string;
|
||||
const strings = bun.strings;
|
||||
const MutableString = bun.MutableString;
|
||||
const stringZ = bun.stringZ;
|
||||
const default_allocator = bun.default_allocator;
|
||||
const JSC = bun.JSC;
|
||||
const Async = bun.Async;
|
||||
const ZigString = JSC.ZigString;
|
||||
const JSValue = JSC.JSValue;
|
||||
const JSGlobalObject = JSC.JSGlobalObject;
|
||||
const CallFrame = JSC.CallFrame;
|
||||
const assert = bun.assert;
|
||||
const EVP = @This();
|
||||
const BoringSSL = bun.BoringSSL.c;
|
||||
56
src/bun.js/api/crypto/HMAC.zig
Normal file
56
src/bun.js/api/crypto/HMAC.zig
Normal file
@@ -0,0 +1,56 @@
|
||||
ctx: BoringSSL.HMAC_CTX,
|
||||
algorithm: EVP.Algorithm,
|
||||
|
||||
pub usingnamespace bun.New(@This());
|
||||
|
||||
pub fn init(algorithm: EVP.Algorithm, key: []const u8) ?*HMAC {
|
||||
const md = algorithm.md() orelse return null;
|
||||
var ctx: BoringSSL.HMAC_CTX = undefined;
|
||||
BoringSSL.HMAC_CTX_init(&ctx);
|
||||
if (BoringSSL.HMAC_Init_ex(&ctx, key.ptr, @intCast(key.len), md, null) != 1) {
|
||||
BoringSSL.HMAC_CTX_cleanup(&ctx);
|
||||
return null;
|
||||
}
|
||||
return HMAC.new(.{
|
||||
.ctx = ctx,
|
||||
.algorithm = algorithm,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn update(this: *HMAC, data: []const u8) void {
|
||||
_ = BoringSSL.HMAC_Update(&this.ctx, data.ptr, data.len);
|
||||
}
|
||||
|
||||
pub fn size(this: *const HMAC) usize {
|
||||
return BoringSSL.HMAC_size(&this.ctx);
|
||||
}
|
||||
|
||||
pub fn copy(this: *HMAC) !*HMAC {
|
||||
var ctx: BoringSSL.HMAC_CTX = undefined;
|
||||
BoringSSL.HMAC_CTX_init(&ctx);
|
||||
if (BoringSSL.HMAC_CTX_copy(&ctx, &this.ctx) != 1) {
|
||||
BoringSSL.HMAC_CTX_cleanup(&ctx);
|
||||
return error.BoringSSLError;
|
||||
}
|
||||
return HMAC.new(.{
|
||||
.ctx = ctx,
|
||||
.algorithm = this.algorithm,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn final(this: *HMAC, out: []u8) []u8 {
|
||||
var outlen: c_uint = undefined;
|
||||
_ = BoringSSL.HMAC_Final(&this.ctx, out.ptr, &outlen);
|
||||
return out[0..outlen];
|
||||
}
|
||||
|
||||
pub fn deinit(this: *HMAC) void {
|
||||
BoringSSL.HMAC_CTX_cleanup(&this.ctx);
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
const bun = @import("root").bun;
|
||||
const BoringSSL = bun.BoringSSL.c;
|
||||
const JSC = bun.JSC;
|
||||
const EVP = JSC.API.Bun.Crypto.EVP;
|
||||
const HMAC = @This();
|
||||
265
src/bun.js/api/crypto/PBKDF2.zig
Normal file
265
src/bun.js/api/crypto/PBKDF2.zig
Normal file
@@ -0,0 +1,265 @@
|
||||
password: JSC.Node.StringOrBuffer = JSC.Node.StringOrBuffer.empty,
|
||||
salt: JSC.Node.StringOrBuffer = JSC.Node.StringOrBuffer.empty,
|
||||
iteration_count: u32 = 1,
|
||||
length: i32 = 0,
|
||||
algorithm: EVP.Algorithm,
|
||||
|
||||
pub fn run(this: *PBKDF2, output: []u8) bool {
|
||||
const password = this.password.slice();
|
||||
const salt = this.salt.slice();
|
||||
const algorithm = this.algorithm;
|
||||
const iteration_count = this.iteration_count;
|
||||
const length = this.length;
|
||||
|
||||
@memset(output, 0);
|
||||
assert(this.length <= @as(i32, @intCast(output.len)));
|
||||
BoringSSL.ERR_clear_error();
|
||||
const rc = BoringSSL.PKCS5_PBKDF2_HMAC(
|
||||
if (password.len > 0) password.ptr else null,
|
||||
@intCast(password.len),
|
||||
salt.ptr,
|
||||
@intCast(salt.len),
|
||||
@intCast(iteration_count),
|
||||
algorithm.md().?,
|
||||
@intCast(length),
|
||||
output.ptr,
|
||||
);
|
||||
|
||||
if (rc <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub const Job = struct {
|
||||
pbkdf2: PBKDF2,
|
||||
output: []u8 = &[_]u8{},
|
||||
task: JSC.WorkPoolTask = .{ .callback = &runTask },
|
||||
promise: JSC.JSPromise.Strong = .{},
|
||||
vm: *JSC.VirtualMachine,
|
||||
err: ?u32 = null,
|
||||
any_task: JSC.AnyTask = undefined,
|
||||
poll: Async.KeepAlive = .{},
|
||||
|
||||
pub usingnamespace bun.New(@This());
|
||||
|
||||
pub fn runTask(task: *JSC.WorkPoolTask) void {
|
||||
const job: *PBKDF2.Job = @fieldParentPtr("task", task);
|
||||
defer job.vm.enqueueTaskConcurrent(JSC.ConcurrentTask.create(job.any_task.task()));
|
||||
job.output = bun.default_allocator.alloc(u8, @as(usize, @intCast(job.pbkdf2.length))) catch {
|
||||
job.err = BoringSSL.EVP_R_MEMORY_LIMIT_EXCEEDED;
|
||||
return;
|
||||
};
|
||||
if (!job.pbkdf2.run(job.output)) {
|
||||
job.err = BoringSSL.ERR_get_error();
|
||||
BoringSSL.ERR_clear_error();
|
||||
|
||||
bun.default_allocator.free(job.output);
|
||||
job.output = &[_]u8{};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn runFromJS(this: *Job) void {
|
||||
defer this.deinit();
|
||||
if (this.vm.isShuttingDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const globalThis = this.vm.global;
|
||||
const promise = this.promise.swap();
|
||||
if (this.err) |err| {
|
||||
promise.reject(globalThis, createCryptoError(globalThis, err));
|
||||
return;
|
||||
}
|
||||
|
||||
const output_slice = this.output;
|
||||
assert(output_slice.len == @as(usize, @intCast(this.pbkdf2.length)));
|
||||
const buffer_value = JSC.JSValue.createBuffer(globalThis, output_slice, bun.default_allocator);
|
||||
if (buffer_value == .zero) {
|
||||
promise.reject(globalThis, ZigString.init("Failed to create buffer").toErrorInstance(globalThis));
|
||||
return;
|
||||
}
|
||||
|
||||
this.output = &[_]u8{};
|
||||
promise.resolve(globalThis, buffer_value);
|
||||
}
|
||||
|
||||
pub fn deinit(this: *Job) void {
|
||||
this.poll.unref(this.vm);
|
||||
this.pbkdf2.deinitAndUnprotect();
|
||||
this.promise.deinit();
|
||||
bun.default_allocator.free(this.output);
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
pub fn create(vm: *JSC.VirtualMachine, globalThis: *JSC.JSGlobalObject, data: *const PBKDF2) *Job {
|
||||
var job = Job.new(.{
|
||||
.pbkdf2 = data.*,
|
||||
.vm = vm,
|
||||
.any_task = undefined,
|
||||
});
|
||||
|
||||
job.promise = JSC.JSPromise.Strong.init(globalThis);
|
||||
job.any_task = JSC.AnyTask.New(@This(), &runFromJS).init(job);
|
||||
job.poll.ref(vm);
|
||||
JSC.WorkPool.schedule(&job.task);
|
||||
|
||||
return job;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn deinitAndUnprotect(this: *PBKDF2) void {
|
||||
this.password.deinitAndUnprotect();
|
||||
this.salt.deinitAndUnprotect();
|
||||
}
|
||||
|
||||
pub fn deinit(this: *PBKDF2) void {
|
||||
this.password.deinit();
|
||||
this.salt.deinit();
|
||||
}
|
||||
|
||||
pub fn fromJS(globalThis: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame, is_async: bool) bun.JSError!PBKDF2 {
|
||||
const arg0, const arg1, const arg2, const arg3, const arg4, const arg5 = callFrame.argumentsAsArray(6);
|
||||
|
||||
if (!arg3.isNumber()) {
|
||||
return globalThis.throwInvalidArgumentTypeValue("keylen", "number", arg3);
|
||||
}
|
||||
|
||||
const keylen_num = arg3.asNumber();
|
||||
|
||||
if (std.math.isInf(keylen_num) or std.math.isNan(keylen_num)) {
|
||||
return globalThis.throwRangeError(keylen_num, .{
|
||||
.field_name = "keylen",
|
||||
.msg = "an integer",
|
||||
});
|
||||
}
|
||||
|
||||
if (keylen_num < 0 or keylen_num > std.math.maxInt(i32)) {
|
||||
return globalThis.throwRangeError(keylen_num, .{ .field_name = "keylen", .min = 0, .max = std.math.maxInt(i32) });
|
||||
}
|
||||
|
||||
const keylen: i32 = @intFromFloat(keylen_num);
|
||||
|
||||
if (globalThis.hasException()) {
|
||||
return error.JSError;
|
||||
}
|
||||
|
||||
if (!arg2.isAnyInt()) {
|
||||
return globalThis.throwInvalidArgumentTypeValue("iterations", "number", arg2);
|
||||
}
|
||||
|
||||
const iteration_count = arg2.coerce(i64, globalThis);
|
||||
|
||||
if (!globalThis.hasException() and (iteration_count < 1 or iteration_count > std.math.maxInt(i32))) {
|
||||
return globalThis.throwRangeError(iteration_count, .{ .field_name = "iterations", .min = 1, .max = std.math.maxInt(i32) + 1 });
|
||||
}
|
||||
|
||||
if (globalThis.hasException()) {
|
||||
return error.JSError;
|
||||
}
|
||||
|
||||
const algorithm = brk: {
|
||||
if (!arg4.isString()) {
|
||||
return globalThis.throwInvalidArgumentTypeValue("digest", "string", arg4);
|
||||
}
|
||||
|
||||
invalid: {
|
||||
switch (try EVP.Algorithm.map.fromJSCaseInsensitive(globalThis, arg4) orelse break :invalid) {
|
||||
.shake128, .shake256, .@"sha3-224", .@"sha3-256", .@"sha3-384", .@"sha3-512" => break :invalid,
|
||||
else => |alg| break :brk alg,
|
||||
}
|
||||
}
|
||||
|
||||
if (!globalThis.hasException()) {
|
||||
const slice = try arg4.toSlice(globalThis, bun.default_allocator);
|
||||
defer slice.deinit();
|
||||
const name = slice.slice();
|
||||
return globalThis.ERR_CRYPTO_INVALID_DIGEST("Invalid digest: {s}", .{name}).throw();
|
||||
}
|
||||
return error.JSError;
|
||||
};
|
||||
|
||||
var out = PBKDF2{
|
||||
.iteration_count = @intCast(iteration_count),
|
||||
.length = keylen,
|
||||
.algorithm = algorithm,
|
||||
};
|
||||
defer {
|
||||
if (globalThis.hasException()) {
|
||||
if (is_async)
|
||||
out.deinitAndUnprotect()
|
||||
else
|
||||
out.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
const allow_string_object = true;
|
||||
out.salt = try JSC.Node.StringOrBuffer.fromJSMaybeAsync(globalThis, bun.default_allocator, arg1, is_async, allow_string_object) orelse {
|
||||
return globalThis.throwInvalidArgumentTypeValue("salt", "string or buffer", arg1);
|
||||
};
|
||||
|
||||
if (out.salt.slice().len > std.math.maxInt(i32)) {
|
||||
return globalThis.throwInvalidArguments("salt is too long", .{});
|
||||
}
|
||||
|
||||
out.password = try JSC.Node.StringOrBuffer.fromJSMaybeAsync(globalThis, bun.default_allocator, arg0, is_async, allow_string_object) orelse {
|
||||
return globalThis.throwInvalidArgumentTypeValue("password", "string or buffer", arg0);
|
||||
};
|
||||
|
||||
if (out.password.slice().len > std.math.maxInt(i32)) {
|
||||
return globalThis.throwInvalidArguments("password is too long", .{});
|
||||
}
|
||||
|
||||
if (is_async) {
|
||||
if (!arg5.isFunction()) {
|
||||
return globalThis.throwInvalidArgumentTypeValue("callback", "function", arg5);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/// For usage in Zig
|
||||
pub fn pbkdf2(
|
||||
output: []u8,
|
||||
password: []const u8,
|
||||
salt: []const u8,
|
||||
iteration_count: u32,
|
||||
algorithm: Algorithm,
|
||||
) ?[]const u8 {
|
||||
var pbk = PBKDF2{
|
||||
.algorithm = algorithm,
|
||||
.password = JSC.Node.StringOrBuffer{ .encoded_slice = JSC.ZigString.Slice.fromUTF8NeverFree(password) },
|
||||
.salt = JSC.Node.StringOrBuffer{ .encoded_slice = JSC.ZigString.Slice.fromUTF8NeverFree(salt) },
|
||||
.iteration_count = iteration_count,
|
||||
.length = @intCast(output.len),
|
||||
};
|
||||
|
||||
if (!pbk.run(output)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
const bun = @import("root").bun;
|
||||
const string = bun.string;
|
||||
const strings = bun.strings;
|
||||
const MutableString = bun.MutableString;
|
||||
const stringZ = bun.stringZ;
|
||||
const default_allocator = bun.default_allocator;
|
||||
const JSC = bun.JSC;
|
||||
const Async = bun.Async;
|
||||
const ZigString = JSC.ZigString;
|
||||
const JSValue = JSC.JSValue;
|
||||
const JSGlobalObject = JSC.JSGlobalObject;
|
||||
const CallFrame = JSC.CallFrame;
|
||||
const assert = bun.assert;
|
||||
const EVP = JSC.API.Bun.Crypto.EVP;
|
||||
const Algorithm = EVP.Algorithm;
|
||||
const BoringSSL = bun.BoringSSL.c;
|
||||
const createCryptoError = JSC.API.Bun.Crypto.createCryptoError;
|
||||
const VirtualMachine = JSC.VirtualMachine;
|
||||
const PBKDF2 = @This();
|
||||
772
src/bun.js/api/crypto/PasswordObject.zig
Normal file
772
src/bun.js/api/crypto/PasswordObject.zig
Normal file
@@ -0,0 +1,772 @@
|
||||
pub const PasswordObject = struct {
|
||||
pub const pwhash = std.crypto.pwhash;
|
||||
pub const Algorithm = enum {
|
||||
argon2i,
|
||||
argon2d,
|
||||
argon2id,
|
||||
bcrypt,
|
||||
|
||||
pub const Value = union(Algorithm) {
|
||||
argon2i: Argon2Params,
|
||||
argon2d: Argon2Params,
|
||||
argon2id: Argon2Params,
|
||||
// bcrypt only accepts "cost"
|
||||
bcrypt: u6,
|
||||
|
||||
pub const bcrpyt_default = 10;
|
||||
|
||||
pub const default = Algorithm.Value{
|
||||
.argon2id = .{},
|
||||
};
|
||||
|
||||
pub fn fromJS(globalObject: *JSC.JSGlobalObject, value: JSC.JSValue) bun.JSError!Value {
|
||||
if (value.isObject()) {
|
||||
if (try value.getTruthy(globalObject, "algorithm")) |algorithm_value| {
|
||||
if (!algorithm_value.isString()) {
|
||||
return globalObject.throwInvalidArgumentType("hash", "algorithm", "string");
|
||||
}
|
||||
|
||||
const algorithm_string = try algorithm_value.getZigString(globalObject);
|
||||
|
||||
switch (PasswordObject.Algorithm.label.getWithEql(algorithm_string, JSC.ZigString.eqlComptime) orelse {
|
||||
return globalObject.throwInvalidArgumentType("hash", "algorithm", unknown_password_algorithm_message);
|
||||
}) {
|
||||
.bcrypt => {
|
||||
var algorithm = PasswordObject.Algorithm.Value{
|
||||
.bcrypt = PasswordObject.Algorithm.Value.bcrpyt_default,
|
||||
};
|
||||
|
||||
if (try value.getTruthy(globalObject, "cost")) |rounds_value| {
|
||||
if (!rounds_value.isNumber()) {
|
||||
return globalObject.throwInvalidArgumentType("hash", "cost", "number");
|
||||
}
|
||||
|
||||
const rounds = rounds_value.coerce(i32, globalObject);
|
||||
|
||||
if (rounds < 4 or rounds > 31) {
|
||||
return globalObject.throwInvalidArguments("Rounds must be between 4 and 31", .{});
|
||||
}
|
||||
|
||||
algorithm.bcrypt = @as(u6, @intCast(rounds));
|
||||
}
|
||||
|
||||
return algorithm;
|
||||
},
|
||||
inline .argon2id, .argon2d, .argon2i => |tag| {
|
||||
var argon = Algorithm.Argon2Params{};
|
||||
|
||||
if (try value.getTruthy(globalObject, "timeCost")) |time_value| {
|
||||
if (!time_value.isNumber()) {
|
||||
return globalObject.throwInvalidArgumentType("hash", "timeCost", "number");
|
||||
}
|
||||
|
||||
const time_cost = time_value.coerce(i32, globalObject);
|
||||
|
||||
if (time_cost < 1) {
|
||||
return globalObject.throwInvalidArguments("Time cost must be greater than 0", .{});
|
||||
}
|
||||
|
||||
argon.time_cost = @as(u32, @intCast(time_cost));
|
||||
}
|
||||
|
||||
if (try value.getTruthy(globalObject, "memoryCost")) |memory_value| {
|
||||
if (!memory_value.isNumber()) {
|
||||
return globalObject.throwInvalidArgumentType("hash", "memoryCost", "number");
|
||||
}
|
||||
|
||||
const memory_cost = memory_value.coerce(i32, globalObject);
|
||||
|
||||
if (memory_cost < 1) {
|
||||
return globalObject.throwInvalidArguments("Memory cost must be greater than 0", .{});
|
||||
}
|
||||
|
||||
argon.memory_cost = @as(u32, @intCast(memory_cost));
|
||||
}
|
||||
|
||||
return @unionInit(Algorithm.Value, @tagName(tag), argon);
|
||||
},
|
||||
}
|
||||
|
||||
unreachable;
|
||||
} else {
|
||||
return globalObject.throwInvalidArgumentType("hash", "options.algorithm", "string");
|
||||
}
|
||||
} else if (value.isString()) {
|
||||
const algorithm_string = try value.getZigString(globalObject);
|
||||
|
||||
switch (PasswordObject.Algorithm.label.getWithEql(algorithm_string, JSC.ZigString.eqlComptime) orelse {
|
||||
return globalObject.throwInvalidArgumentType("hash", "algorithm", unknown_password_algorithm_message);
|
||||
}) {
|
||||
.bcrypt => {
|
||||
return PasswordObject.Algorithm.Value{
|
||||
.bcrypt = PasswordObject.Algorithm.Value.bcrpyt_default,
|
||||
};
|
||||
},
|
||||
.argon2id => {
|
||||
return PasswordObject.Algorithm.Value{
|
||||
.argon2id = .{},
|
||||
};
|
||||
},
|
||||
.argon2d => {
|
||||
return PasswordObject.Algorithm.Value{
|
||||
.argon2d = .{},
|
||||
};
|
||||
},
|
||||
.argon2i => {
|
||||
return PasswordObject.Algorithm.Value{
|
||||
.argon2i = .{},
|
||||
};
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return globalObject.throwInvalidArgumentType("hash", "algorithm", "string");
|
||||
}
|
||||
|
||||
unreachable;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Argon2Params = struct {
|
||||
// we don't support the other options right now, but can add them later if someone asks
|
||||
memory_cost: u32 = pwhash.argon2.Params.interactive_2id.m,
|
||||
time_cost: u32 = pwhash.argon2.Params.interactive_2id.t,
|
||||
|
||||
pub fn toParams(this: Argon2Params) pwhash.argon2.Params {
|
||||
return pwhash.argon2.Params{
|
||||
.t = this.time_cost,
|
||||
.m = this.memory_cost,
|
||||
.p = 1,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const argon2 = Algorithm.argon2id;
|
||||
|
||||
pub const label = bun.ComptimeStringMap(
|
||||
Algorithm,
|
||||
.{
|
||||
.{ "argon2i", .argon2i },
|
||||
.{ "argon2d", .argon2d },
|
||||
.{ "argon2id", .argon2id },
|
||||
.{ "bcrypt", .bcrypt },
|
||||
},
|
||||
);
|
||||
|
||||
pub const default = Algorithm.argon2;
|
||||
|
||||
pub fn get(pw: []const u8) ?Algorithm {
|
||||
if (pw[0] != '$') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// PHC format looks like $<algorithm>$<params>$<salt>$<hash><optional stuff>
|
||||
if (strings.hasPrefixComptime(pw[1..], "argon2d$")) {
|
||||
return .argon2d;
|
||||
}
|
||||
if (strings.hasPrefixComptime(pw[1..], "argon2i$")) {
|
||||
return .argon2i;
|
||||
}
|
||||
if (strings.hasPrefixComptime(pw[1..], "argon2id$")) {
|
||||
return .argon2id;
|
||||
}
|
||||
|
||||
if (strings.hasPrefixComptime(pw[1..], "bcrypt")) {
|
||||
return .bcrypt;
|
||||
}
|
||||
|
||||
// https://en.wikipedia.org/wiki/Crypt_(C)
|
||||
if (strings.hasPrefixComptime(pw[1..], "2")) {
|
||||
return .bcrypt;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
pub const HashError = pwhash.Error || error{UnsupportedAlgorithm};
|
||||
|
||||
// This is purposely simple because nobody asked to make it more complicated
|
||||
pub fn hash(
|
||||
allocator: std.mem.Allocator,
|
||||
password: []const u8,
|
||||
algorithm: Algorithm.Value,
|
||||
) HashError![]const u8 {
|
||||
switch (algorithm) {
|
||||
inline .argon2i, .argon2d, .argon2id => |argon| {
|
||||
var outbuf: [4096]u8 = undefined;
|
||||
const hash_options = pwhash.argon2.HashOptions{
|
||||
.params = argon.toParams(),
|
||||
.allocator = allocator,
|
||||
.mode = switch (algorithm) {
|
||||
.argon2i => .argon2i,
|
||||
.argon2d => .argon2d,
|
||||
.argon2id => .argon2id,
|
||||
else => unreachable,
|
||||
},
|
||||
.encoding = .phc,
|
||||
};
|
||||
// warning: argon2's code may spin up threads if paralellism is set to > 0
|
||||
// we don't expose this option
|
||||
// but since it parses from phc format, it's possible that it will be set
|
||||
// eventually we should do something that about that.
|
||||
const out_bytes = try pwhash.argon2.strHash(password, hash_options, &outbuf);
|
||||
return try allocator.dupe(u8, out_bytes);
|
||||
},
|
||||
.bcrypt => |cost| {
|
||||
var outbuf: [4096]u8 = undefined;
|
||||
var outbuf_slice: []u8 = outbuf[0..];
|
||||
var password_to_use = password;
|
||||
// bcrypt silently truncates passwords longer than 72 bytes
|
||||
// we use SHA512 to hash the password if it's longer than 72 bytes
|
||||
if (password.len > 72) {
|
||||
var sha_512 = bun.sha.SHA512.init();
|
||||
defer sha_512.deinit();
|
||||
sha_512.update(password);
|
||||
sha_512.final(outbuf[0..bun.sha.SHA512.digest]);
|
||||
password_to_use = outbuf[0..bun.sha.SHA512.digest];
|
||||
outbuf_slice = outbuf[bun.sha.SHA512.digest..];
|
||||
}
|
||||
|
||||
const hash_options = pwhash.bcrypt.HashOptions{
|
||||
.params = pwhash.bcrypt.Params{
|
||||
.rounds_log = cost,
|
||||
.silently_truncate_password = true,
|
||||
},
|
||||
.allocator = allocator,
|
||||
.encoding = .crypt,
|
||||
};
|
||||
const out_bytes = try pwhash.bcrypt.strHash(password_to_use, hash_options, outbuf_slice);
|
||||
return try allocator.dupe(u8, out_bytes);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn verify(
|
||||
allocator: std.mem.Allocator,
|
||||
password: []const u8,
|
||||
previous_hash: []const u8,
|
||||
algorithm: ?Algorithm,
|
||||
) HashError!bool {
|
||||
if (previous_hash.len == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return verifyWithAlgorithm(
|
||||
allocator,
|
||||
password,
|
||||
previous_hash,
|
||||
algorithm orelse Algorithm.get(previous_hash) orelse return error.UnsupportedAlgorithm,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn verifyWithAlgorithm(
|
||||
allocator: std.mem.Allocator,
|
||||
password: []const u8,
|
||||
previous_hash: []const u8,
|
||||
algorithm: Algorithm,
|
||||
) HashError!bool {
|
||||
switch (algorithm) {
|
||||
.argon2id, .argon2d, .argon2i => {
|
||||
pwhash.argon2.strVerify(previous_hash, password, .{ .allocator = allocator }) catch |err| {
|
||||
if (err == error.PasswordVerificationFailed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return err;
|
||||
};
|
||||
return true;
|
||||
},
|
||||
.bcrypt => {
|
||||
var password_to_use = password;
|
||||
var outbuf: [bun.sha.SHA512.digest]u8 = undefined;
|
||||
|
||||
// bcrypt silently truncates passwords longer than 72 bytes
|
||||
// we use SHA512 to hash the password if it's longer than 72 bytes
|
||||
if (password.len > 72) {
|
||||
var sha_512 = bun.sha.SHA512.init();
|
||||
defer sha_512.deinit();
|
||||
sha_512.update(password);
|
||||
sha_512.final(&outbuf);
|
||||
password_to_use = &outbuf;
|
||||
}
|
||||
pwhash.bcrypt.strVerify(previous_hash, password_to_use, .{
|
||||
.allocator = allocator,
|
||||
.silently_truncate_password = true,
|
||||
}) catch |err| {
|
||||
if (err == error.PasswordVerificationFailed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return err;
|
||||
};
|
||||
return true;
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub const JSPasswordObject = struct {
|
||||
const PascalToUpperUnderscoreCaseFormatter = struct {
|
||||
input: []const u8,
|
||||
pub fn format(self: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
|
||||
for (self.input) |c| {
|
||||
if (std.ascii.isUpper(c)) {
|
||||
try writer.writeByte('_');
|
||||
try writer.writeByte(c);
|
||||
} else if (std.ascii.isLower(c)) {
|
||||
try writer.writeByte(std.ascii.toUpper(c));
|
||||
} else {
|
||||
try writer.writeByte(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub export fn JSPasswordObject__create(globalObject: *JSC.JSGlobalObject) JSC.JSValue {
|
||||
var object = JSValue.createEmptyObject(globalObject, 4);
|
||||
object.put(
|
||||
globalObject,
|
||||
ZigString.static("hash"),
|
||||
JSC.createCallback(globalObject, ZigString.static("hash"), 2, JSPasswordObject__hash),
|
||||
);
|
||||
object.put(
|
||||
globalObject,
|
||||
ZigString.static("hashSync"),
|
||||
JSC.createCallback(globalObject, ZigString.static("hashSync"), 2, JSPasswordObject__hashSync),
|
||||
);
|
||||
object.put(
|
||||
globalObject,
|
||||
ZigString.static("verify"),
|
||||
JSC.createCallback(globalObject, ZigString.static("verify"), 2, JSPasswordObject__verify),
|
||||
);
|
||||
object.put(
|
||||
globalObject,
|
||||
ZigString.static("verifySync"),
|
||||
JSC.createCallback(globalObject, ZigString.static("verifySync"), 2, JSPasswordObject__verifySync),
|
||||
);
|
||||
return object;
|
||||
}
|
||||
|
||||
const HashJob = struct {
|
||||
algorithm: PasswordObject.Algorithm.Value,
|
||||
password: []const u8,
|
||||
promise: JSC.JSPromise.Strong,
|
||||
event_loop: *JSC.EventLoop,
|
||||
global: *JSC.JSGlobalObject,
|
||||
ref: Async.KeepAlive = .{},
|
||||
task: JSC.WorkPoolTask = .{ .callback = &run },
|
||||
|
||||
pub usingnamespace bun.New(@This());
|
||||
|
||||
pub const Result = struct {
|
||||
value: Value,
|
||||
ref: Async.KeepAlive = .{},
|
||||
|
||||
task: JSC.AnyTask = undefined,
|
||||
promise: JSC.JSPromise.Strong,
|
||||
global: *JSC.JSGlobalObject,
|
||||
|
||||
pub usingnamespace bun.New(@This());
|
||||
|
||||
pub const Value = union(enum) {
|
||||
err: PasswordObject.HashError,
|
||||
hash: []const u8,
|
||||
|
||||
pub fn toErrorInstance(this: Value, globalObject: *JSC.JSGlobalObject) JSC.JSValue {
|
||||
const error_code = std.fmt.allocPrint(bun.default_allocator, "PASSWORD{}", .{PascalToUpperUnderscoreCaseFormatter{ .input = @errorName(this.err) }}) catch bun.outOfMemory();
|
||||
defer bun.default_allocator.free(error_code);
|
||||
const instance = globalObject.createErrorInstance("Password hashing failed with error \"{s}\"", .{@errorName(this.err)});
|
||||
instance.put(globalObject, ZigString.static("code"), JSC.ZigString.init(error_code).toJS(globalObject));
|
||||
return instance;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn runFromJS(this: *Result) void {
|
||||
var promise = this.promise;
|
||||
defer promise.deinit();
|
||||
this.promise = .{};
|
||||
this.ref.unref(this.global.bunVM());
|
||||
const global = this.global;
|
||||
switch (this.value) {
|
||||
.err => {
|
||||
const error_instance = this.value.toErrorInstance(global);
|
||||
this.destroy();
|
||||
promise.reject(global, error_instance);
|
||||
},
|
||||
.hash => |value| {
|
||||
const js_string = JSC.ZigString.init(value).toJS(global);
|
||||
this.destroy();
|
||||
promise.resolve(global, js_string);
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn deinit(this: *HashJob) void {
|
||||
this.promise.deinit();
|
||||
bun.freeSensitive(bun.default_allocator, this.password);
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
pub fn getValue(password: []const u8, algorithm: PasswordObject.Algorithm.Value) Result.Value {
|
||||
const value = PasswordObject.hash(bun.default_allocator, password, algorithm) catch |err| {
|
||||
return Result.Value{ .err = err };
|
||||
};
|
||||
return Result.Value{ .hash = value };
|
||||
}
|
||||
|
||||
pub fn run(task: *bun.ThreadPool.Task) void {
|
||||
var this: *HashJob = @fieldParentPtr("task", task);
|
||||
|
||||
var result = Result.new(.{
|
||||
.value = getValue(this.password, this.algorithm),
|
||||
.task = undefined,
|
||||
.promise = this.promise,
|
||||
.global = this.global,
|
||||
.ref = this.ref,
|
||||
});
|
||||
this.promise = .empty;
|
||||
|
||||
result.task = JSC.AnyTask.New(Result, Result.runFromJS).init(result);
|
||||
this.ref = .{};
|
||||
this.event_loop.enqueueTaskConcurrent(JSC.ConcurrentTask.createFrom(&result.task));
|
||||
this.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
pub fn hash(globalObject: *JSC.JSGlobalObject, password: []const u8, algorithm: PasswordObject.Algorithm.Value, comptime sync: bool) bun.JSError!JSC.JSValue {
|
||||
assert(password.len > 0); // caller must check
|
||||
|
||||
if (comptime sync) {
|
||||
const value = HashJob.getValue(password, algorithm);
|
||||
switch (value) {
|
||||
.err => {
|
||||
const error_instance = value.toErrorInstance(globalObject);
|
||||
return globalObject.throwValue(error_instance);
|
||||
},
|
||||
.hash => |h| {
|
||||
return JSC.ZigString.init(h).toJS(globalObject);
|
||||
},
|
||||
}
|
||||
|
||||
unreachable;
|
||||
}
|
||||
|
||||
const promise = JSC.JSPromise.Strong.init(globalObject);
|
||||
|
||||
var job = HashJob.new(.{
|
||||
.algorithm = algorithm,
|
||||
.password = password,
|
||||
.promise = promise,
|
||||
.event_loop = globalObject.bunVM().eventLoop(),
|
||||
.global = globalObject,
|
||||
});
|
||||
job.ref.ref(globalObject.bunVM());
|
||||
JSC.WorkPool.schedule(&job.task);
|
||||
|
||||
return promise.value();
|
||||
}
|
||||
|
||||
pub fn verify(globalObject: *JSC.JSGlobalObject, password: []const u8, prev_hash: []const u8, algorithm: ?PasswordObject.Algorithm, comptime sync: bool) bun.JSError!JSC.JSValue {
|
||||
assert(password.len > 0); // caller must check
|
||||
|
||||
if (comptime sync) {
|
||||
const value = VerifyJob.getValue(password, prev_hash, algorithm);
|
||||
switch (value) {
|
||||
.err => {
|
||||
const error_instance = value.toErrorInstance(globalObject);
|
||||
return globalObject.throwValue(error_instance);
|
||||
},
|
||||
.pass => |pass| {
|
||||
return JSC.JSValue.jsBoolean(pass);
|
||||
},
|
||||
}
|
||||
|
||||
unreachable;
|
||||
}
|
||||
|
||||
var promise = JSC.JSPromise.Strong.init(globalObject);
|
||||
|
||||
const job = VerifyJob.new(.{
|
||||
.algorithm = algorithm,
|
||||
.password = password,
|
||||
.prev_hash = prev_hash,
|
||||
.promise = promise,
|
||||
.event_loop = globalObject.bunVM().eventLoop(),
|
||||
.global = globalObject,
|
||||
});
|
||||
job.ref.ref(globalObject.bunVM());
|
||||
JSC.WorkPool.schedule(&job.task);
|
||||
|
||||
return promise.value();
|
||||
}
|
||||
|
||||
// Once we have bindings generator, this should be replaced with a generated function
|
||||
pub fn JSPasswordObject__hash(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
|
||||
const arguments_ = callframe.arguments_old(2);
|
||||
const arguments = arguments_.ptr[0..arguments_.len];
|
||||
|
||||
if (arguments.len < 1) {
|
||||
return globalObject.throwNotEnoughArguments("hash", 1, 0);
|
||||
}
|
||||
|
||||
var algorithm = PasswordObject.Algorithm.Value.default;
|
||||
|
||||
if (arguments.len > 1 and !arguments[1].isEmptyOrUndefinedOrNull()) {
|
||||
algorithm = try PasswordObject.Algorithm.Value.fromJS(globalObject, arguments[1]);
|
||||
}
|
||||
|
||||
// TODO: this most likely should error like `hashSync` instead of stringifying.
|
||||
//
|
||||
// fromJS(...) orelse {
|
||||
// return globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray");
|
||||
// }
|
||||
const password_to_hash = try JSC.Node.StringOrBuffer.fromJSToOwnedSlice(globalObject, arguments[0], bun.default_allocator);
|
||||
errdefer bun.default_allocator.free(password_to_hash);
|
||||
|
||||
if (password_to_hash.len == 0) {
|
||||
return globalObject.throwInvalidArguments("password must not be empty", .{});
|
||||
}
|
||||
|
||||
return hash(globalObject, password_to_hash, algorithm, false);
|
||||
}
|
||||
|
||||
// Once we have bindings generator, this should be replaced with a generated function
|
||||
pub fn JSPasswordObject__hashSync(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
|
||||
const arguments_ = callframe.arguments_old(2);
|
||||
const arguments = arguments_.ptr[0..arguments_.len];
|
||||
|
||||
if (arguments.len < 1) {
|
||||
return globalObject.throwNotEnoughArguments("hash", 1, 0);
|
||||
}
|
||||
|
||||
var algorithm = PasswordObject.Algorithm.Value.default;
|
||||
|
||||
if (arguments.len > 1 and !arguments[1].isEmptyOrUndefinedOrNull()) {
|
||||
algorithm = try PasswordObject.Algorithm.Value.fromJS(globalObject, arguments[1]);
|
||||
}
|
||||
|
||||
var string_or_buffer = try JSC.Node.StringOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[0]) orelse {
|
||||
return globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray");
|
||||
};
|
||||
defer string_or_buffer.deinit();
|
||||
|
||||
if (string_or_buffer.slice().len == 0) {
|
||||
return globalObject.throwInvalidArguments("password must not be empty", .{});
|
||||
}
|
||||
|
||||
return hash(globalObject, string_or_buffer.slice(), algorithm, true);
|
||||
}
|
||||
|
||||
const VerifyJob = struct {
|
||||
algorithm: ?PasswordObject.Algorithm = null,
|
||||
password: []const u8,
|
||||
prev_hash: []const u8,
|
||||
promise: JSC.JSPromise.Strong,
|
||||
event_loop: *JSC.EventLoop,
|
||||
global: *JSC.JSGlobalObject,
|
||||
ref: Async.KeepAlive = .{},
|
||||
task: JSC.WorkPoolTask = .{ .callback = &run },
|
||||
|
||||
pub usingnamespace bun.New(@This());
|
||||
|
||||
pub const Result = struct {
|
||||
value: Value,
|
||||
ref: Async.KeepAlive = .{},
|
||||
|
||||
task: JSC.AnyTask = undefined,
|
||||
promise: JSC.JSPromise.Strong,
|
||||
global: *JSC.JSGlobalObject,
|
||||
|
||||
pub usingnamespace bun.New(@This());
|
||||
|
||||
pub const Value = union(enum) {
|
||||
err: PasswordObject.HashError,
|
||||
pass: bool,
|
||||
|
||||
pub fn toErrorInstance(this: Value, globalObject: *JSC.JSGlobalObject) JSC.JSValue {
|
||||
const error_code = std.fmt.allocPrint(bun.default_allocator, "PASSWORD{}", .{PascalToUpperUnderscoreCaseFormatter{ .input = @errorName(this.err) }}) catch bun.outOfMemory();
|
||||
defer bun.default_allocator.free(error_code);
|
||||
const instance = globalObject.createErrorInstance("Password verification failed with error \"{s}\"", .{@errorName(this.err)});
|
||||
instance.put(globalObject, ZigString.static("code"), JSC.ZigString.init(error_code).toJS(globalObject));
|
||||
return instance;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn runFromJS(this: *Result) void {
|
||||
var promise = this.promise;
|
||||
defer promise.deinit();
|
||||
this.promise = .{};
|
||||
this.ref.unref(this.global.bunVM());
|
||||
const global = this.global;
|
||||
switch (this.value) {
|
||||
.err => {
|
||||
const error_instance = this.value.toErrorInstance(global);
|
||||
this.destroy();
|
||||
promise.reject(global, error_instance);
|
||||
},
|
||||
.pass => |pass| {
|
||||
this.destroy();
|
||||
promise.resolve(global, JSC.JSValue.jsBoolean(pass));
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn deinit(this: *VerifyJob) void {
|
||||
this.promise.deinit();
|
||||
|
||||
bun.freeSensitive(bun.default_allocator, this.password);
|
||||
bun.freeSensitive(bun.default_allocator, this.prev_hash);
|
||||
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
pub fn getValue(password: []const u8, prev_hash: []const u8, algorithm: ?PasswordObject.Algorithm) Result.Value {
|
||||
const pass = PasswordObject.verify(bun.default_allocator, password, prev_hash, algorithm) catch |err| {
|
||||
return Result.Value{ .err = err };
|
||||
};
|
||||
return Result.Value{ .pass = pass };
|
||||
}
|
||||
|
||||
pub fn run(task: *bun.ThreadPool.Task) void {
|
||||
var this: *VerifyJob = @fieldParentPtr("task", task);
|
||||
|
||||
var result = Result.new(.{
|
||||
.value = getValue(this.password, this.prev_hash, this.algorithm),
|
||||
.task = undefined,
|
||||
.promise = this.promise,
|
||||
.global = this.global,
|
||||
.ref = this.ref,
|
||||
});
|
||||
this.promise = .empty;
|
||||
|
||||
result.task = JSC.AnyTask.New(Result, Result.runFromJS).init(result);
|
||||
this.ref = .{};
|
||||
this.event_loop.enqueueTaskConcurrent(JSC.ConcurrentTask.createFrom(&result.task));
|
||||
this.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
// Once we have bindings generator, this should be replaced with a generated function
|
||||
pub fn JSPasswordObject__verify(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
|
||||
const arguments_ = callframe.arguments_old(3);
|
||||
const arguments = arguments_.ptr[0..arguments_.len];
|
||||
|
||||
if (arguments.len < 2) {
|
||||
return globalObject.throwNotEnoughArguments("verify", 2, 0);
|
||||
}
|
||||
|
||||
var algorithm: ?PasswordObject.Algorithm = null;
|
||||
|
||||
if (arguments.len > 2 and !arguments[2].isEmptyOrUndefinedOrNull()) {
|
||||
if (!arguments[2].isString()) {
|
||||
return globalObject.throwInvalidArgumentType("verify", "algorithm", "string");
|
||||
}
|
||||
|
||||
const algorithm_string = try arguments[2].getZigString(globalObject);
|
||||
|
||||
algorithm = PasswordObject.Algorithm.label.getWithEql(algorithm_string, JSC.ZigString.eqlComptime) orelse {
|
||||
if (!globalObject.hasException()) {
|
||||
return globalObject.throwInvalidArgumentType("verify", "algorithm", unknown_password_algorithm_message);
|
||||
}
|
||||
return error.JSError;
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: this most likely should error like `verifySync` instead of stringifying.
|
||||
//
|
||||
// fromJS(...) orelse {
|
||||
// return globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray");
|
||||
// }
|
||||
const owned_password = try JSC.Node.StringOrBuffer.fromJSToOwnedSlice(globalObject, arguments[0], bun.default_allocator);
|
||||
|
||||
// TODO: this most likely should error like `verifySync` instead of stringifying.
|
||||
//
|
||||
// fromJS(...) orelse {
|
||||
// return globalObject.throwInvalidArgumentType("hash", "password", "string or TypedArray");
|
||||
// }
|
||||
const owned_hash = JSC.Node.StringOrBuffer.fromJSToOwnedSlice(globalObject, arguments[1], bun.default_allocator) catch |err| {
|
||||
bun.default_allocator.free(owned_password);
|
||||
return err;
|
||||
};
|
||||
|
||||
if (owned_hash.len == 0) {
|
||||
bun.default_allocator.free(owned_password);
|
||||
return JSC.JSPromise.resolvedPromiseValue(globalObject, JSC.JSValue.jsBoolean(false));
|
||||
}
|
||||
|
||||
if (owned_password.len == 0) {
|
||||
bun.default_allocator.free(owned_hash);
|
||||
return JSC.JSPromise.resolvedPromiseValue(globalObject, JSC.JSValue.jsBoolean(false));
|
||||
}
|
||||
|
||||
return verify(globalObject, owned_password, owned_hash, algorithm, false);
|
||||
}
|
||||
|
||||
// Once we have bindings generator, this should be replaced with a generated function
|
||||
pub fn JSPasswordObject__verifySync(globalObject: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSC.JSValue {
|
||||
const arguments_ = callframe.arguments_old(3);
|
||||
const arguments = arguments_.ptr[0..arguments_.len];
|
||||
|
||||
if (arguments.len < 2) {
|
||||
return globalObject.throwNotEnoughArguments("verify", 2, 0);
|
||||
}
|
||||
|
||||
var algorithm: ?PasswordObject.Algorithm = null;
|
||||
|
||||
if (arguments.len > 2 and !arguments[2].isEmptyOrUndefinedOrNull()) {
|
||||
if (!arguments[2].isString()) {
|
||||
return globalObject.throwInvalidArgumentType("verify", "algorithm", "string");
|
||||
}
|
||||
|
||||
const algorithm_string = try arguments[2].getZigString(globalObject);
|
||||
|
||||
algorithm = PasswordObject.Algorithm.label.getWithEql(algorithm_string, JSC.ZigString.eqlComptime) orelse {
|
||||
if (!globalObject.hasException()) {
|
||||
return globalObject.throwInvalidArgumentType("verify", "algorithm", unknown_password_algorithm_message);
|
||||
}
|
||||
return .zero;
|
||||
};
|
||||
}
|
||||
|
||||
var password = try JSC.Node.StringOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[0]) orelse {
|
||||
return globalObject.throwInvalidArgumentType("verify", "password", "string or TypedArray");
|
||||
};
|
||||
|
||||
var hash_ = try JSC.Node.StringOrBuffer.fromJS(globalObject, bun.default_allocator, arguments[1]) orelse {
|
||||
password.deinit();
|
||||
return globalObject.throwInvalidArgumentType("verify", "hash", "string or TypedArray");
|
||||
};
|
||||
|
||||
defer password.deinit();
|
||||
defer hash_.deinit();
|
||||
|
||||
if (hash_.slice().len == 0) {
|
||||
return JSC.JSValue.jsBoolean(false);
|
||||
}
|
||||
|
||||
if (password.slice().len == 0) {
|
||||
return JSC.JSValue.jsBoolean(false);
|
||||
}
|
||||
|
||||
return verify(globalObject, password.slice(), hash_.slice(), algorithm, true);
|
||||
}
|
||||
};
|
||||
|
||||
const std = @import("std");
|
||||
const bun = @import("root").bun;
|
||||
const string = bun.string;
|
||||
const strings = bun.strings;
|
||||
const MutableString = bun.MutableString;
|
||||
const stringZ = bun.stringZ;
|
||||
const default_allocator = bun.default_allocator;
|
||||
const JSC = bun.JSC;
|
||||
const Async = bun.Async;
|
||||
const ZigString = JSC.ZigString;
|
||||
const JSValue = JSC.JSValue;
|
||||
const JSGlobalObject = JSC.JSGlobalObject;
|
||||
const CallFrame = JSC.CallFrame;
|
||||
const assert = bun.assert;
|
||||
|
||||
const unknown_password_algorithm_message = "unknown algorithm, expected one of: \"bcrypt\", \"argon2id\", \"argon2d\", \"argon2i\" (default is \"argon2id\")";
|
||||
File diff suppressed because it is too large
Load Diff
1065
src/bun.js/api/server/NodeHTTPResponse.zig
Normal file
1065
src/bun.js/api/server/NodeHTTPResponse.zig
Normal file
File diff suppressed because it is too large
Load Diff
1297
src/bun.js/api/server/ServerWebSocket.zig
Normal file
1297
src/bun.js/api/server/ServerWebSocket.zig
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1302,7 +1302,7 @@ pub const JestPrettyFormat = struct {
|
||||
} else if (value.as(JSC.ResolveMessage)) |resolve_log| {
|
||||
resolve_log.msg.writeFormat(writer_, enable_ansi_colors) catch {};
|
||||
return;
|
||||
} else if (printAsymmetricMatcher(this, Format, &writer, writer_, name_buf, value, enable_ansi_colors)) {
|
||||
} else if (printAsymmetricMatcher(this, &writer, writer_, &name_buf, value, enable_ansi_colors)) {
|
||||
return;
|
||||
} else if (jsType != .DOMWrapper) {
|
||||
if (value.isCallable()) {
|
||||
@@ -2041,18 +2041,15 @@ pub const JestPrettyFormat = struct {
|
||||
pub fn printAsymmetricMatcher(
|
||||
// the Formatter instance
|
||||
this: anytype,
|
||||
comptime Format: anytype,
|
||||
/// The WrappedWriter
|
||||
writer: anytype,
|
||||
/// The raw writer
|
||||
writer_: anytype,
|
||||
/// Buf used to print strings
|
||||
name_buf: [512]u8,
|
||||
name_buf: *[512]u8,
|
||||
value: JSValue,
|
||||
comptime enable_ansi_colors: bool,
|
||||
) bool {
|
||||
_ = Format;
|
||||
|
||||
if (value.as(expect.ExpectAnything)) |matcher| {
|
||||
printAsymmetricMatcherPromisePrefix(matcher.flags, this, writer);
|
||||
if (matcher.flags.not) {
|
||||
@@ -2074,7 +2071,7 @@ pub const JestPrettyFormat = struct {
|
||||
writer.writeAll("Any<");
|
||||
}
|
||||
|
||||
var class_name = ZigString.init(&name_buf);
|
||||
var class_name = ZigString.init(name_buf);
|
||||
constructor_value.getClassName(this.globalThis, &class_name);
|
||||
this.addForNewLine(class_name.len);
|
||||
writer.print(comptime Output.prettyFmt("<cyan>{}<r>", enable_ansi_colors), .{class_name});
|
||||
|
||||
160
src/bun.js/webcore/EncodingLabel.zig
Normal file
160
src/bun.js/webcore/EncodingLabel.zig
Normal file
@@ -0,0 +1,160 @@
|
||||
/// https://encoding.spec.whatwg.org/encodings.json
|
||||
pub const EncodingLabel = enum {
|
||||
@"UTF-8",
|
||||
IBM866,
|
||||
@"ISO-8859-2",
|
||||
@"ISO-8859-3",
|
||||
@"ISO-8859-4",
|
||||
@"ISO-8859-5",
|
||||
@"ISO-8859-6",
|
||||
@"ISO-8859-7",
|
||||
@"ISO-8859-8",
|
||||
@"ISO-8859-8-I",
|
||||
@"ISO-8859-10",
|
||||
@"ISO-8859-13",
|
||||
@"ISO-8859-14",
|
||||
@"ISO-8859-15",
|
||||
@"ISO-8859-16",
|
||||
@"KOI8-R",
|
||||
@"KOI8-U",
|
||||
macintosh,
|
||||
@"windows-874",
|
||||
@"windows-1250",
|
||||
@"windows-1251",
|
||||
/// Also known as
|
||||
/// - ASCII
|
||||
/// - latin1
|
||||
@"windows-1252",
|
||||
@"windows-1253",
|
||||
@"windows-1254",
|
||||
@"windows-1255",
|
||||
@"windows-1256",
|
||||
@"windows-1257",
|
||||
@"windows-1258",
|
||||
@"x-mac-cyrillic",
|
||||
Big5,
|
||||
@"EUC-JP",
|
||||
@"ISO-2022-JP",
|
||||
Shift_JIS,
|
||||
@"EUC-KR",
|
||||
@"UTF-16BE",
|
||||
@"UTF-16LE",
|
||||
@"x-user-defined",
|
||||
|
||||
pub const Map = std.enums.EnumMap(EncodingLabel, string);
|
||||
pub const label: Map = brk: {
|
||||
var map = Map.initFull("");
|
||||
map.put(EncodingLabel.@"UTF-8", "utf-8");
|
||||
map.put(EncodingLabel.@"UTF-16LE", "utf-16le");
|
||||
map.put(EncodingLabel.@"windows-1252", "windows-1252");
|
||||
break :brk map;
|
||||
};
|
||||
|
||||
const utf16_names = [_]string{
|
||||
"ucs-2",
|
||||
"utf-16",
|
||||
"unicode",
|
||||
"utf-16le",
|
||||
"csunicode",
|
||||
"unicodefeff",
|
||||
"iso-10646-ucs-2",
|
||||
};
|
||||
|
||||
const utf8_names = [_]string{
|
||||
"utf8",
|
||||
"utf-8",
|
||||
"unicode11utf8",
|
||||
"unicode20utf8",
|
||||
"x-unicode20utf8",
|
||||
"unicode-1-1-utf-8",
|
||||
};
|
||||
|
||||
const latin1_names = [_]string{
|
||||
"l1",
|
||||
"ascii",
|
||||
"cp819",
|
||||
"cp1252",
|
||||
"ibm819",
|
||||
"latin1",
|
||||
"iso88591",
|
||||
"us-ascii",
|
||||
"x-cp1252",
|
||||
"iso8859-1",
|
||||
"iso_8859-1",
|
||||
"iso-8859-1",
|
||||
"iso-ir-100",
|
||||
"csisolatin1",
|
||||
"windows-1252",
|
||||
"ansi_x3.4-1968",
|
||||
"iso_8859-1:1987",
|
||||
};
|
||||
|
||||
pub const latin1 = EncodingLabel.@"windows-1252";
|
||||
|
||||
pub fn which(input_: string) ?EncodingLabel {
|
||||
const input = strings.trim(input_, " \t\r\n");
|
||||
const ExactMatcher = strings.ExactSizeMatcher;
|
||||
const Eight = ExactMatcher(8);
|
||||
const Sixteen = ExactMatcher(16);
|
||||
return switch (input.len) {
|
||||
1, 0 => null,
|
||||
2...8 => switch (Eight.matchLower(input)) {
|
||||
Eight.case("l1"),
|
||||
Eight.case("ascii"),
|
||||
Eight.case("cp819"),
|
||||
Eight.case("cp1252"),
|
||||
Eight.case("ibm819"),
|
||||
Eight.case("latin1"),
|
||||
Eight.case("iso88591"),
|
||||
Eight.case("us-ascii"),
|
||||
Eight.case("x-cp1252"),
|
||||
=> EncodingLabel.latin1,
|
||||
|
||||
Eight.case("ucs-2"),
|
||||
Eight.case("utf-16"),
|
||||
Eight.case("unicode"),
|
||||
Eight.case("utf-16le"),
|
||||
=> EncodingLabel.@"UTF-16LE",
|
||||
|
||||
Eight.case("utf-16be"),
|
||||
=> EncodingLabel.@"UTF-16BE",
|
||||
|
||||
Eight.case("utf8"), Eight.case("utf-8") => EncodingLabel.@"UTF-8",
|
||||
else => null,
|
||||
},
|
||||
|
||||
9...16 => switch (Sixteen.matchLower(input)) {
|
||||
Sixteen.case("iso8859-1"),
|
||||
Sixteen.case("iso_8859-1"),
|
||||
Sixteen.case("iso-8859-1"),
|
||||
Sixteen.case("iso-ir-100"),
|
||||
Sixteen.case("csisolatin1"),
|
||||
Sixteen.case("windows-1252"),
|
||||
Sixteen.case("ansi_x3.4-1968"),
|
||||
Sixteen.case("iso_8859-1:1987"),
|
||||
=> EncodingLabel.latin1,
|
||||
|
||||
Sixteen.case("unicode11utf8"),
|
||||
Sixteen.case("unicode20utf8"),
|
||||
Sixteen.case("x-unicode20utf8"),
|
||||
=> EncodingLabel.@"UTF-8",
|
||||
|
||||
Sixteen.case("csunicode"),
|
||||
Sixteen.case("unicodefeff"),
|
||||
Sixteen.case("iso-10646-ucs-2"),
|
||||
=> EncodingLabel.@"UTF-16LE",
|
||||
|
||||
else => null,
|
||||
},
|
||||
else => if (strings.eqlCaseInsensitiveASCII(input, "unicode-1-1-utf-8", true))
|
||||
EncodingLabel.@"UTF-8"
|
||||
else
|
||||
null,
|
||||
};
|
||||
}
|
||||
};
|
||||
const std = @import("std");
|
||||
const bun = @import("root").bun;
|
||||
const encoding = @import("encoding.zig");
|
||||
const string = []const u8;
|
||||
const strings = bun.strings;
|
||||
350
src/bun.js/webcore/TextDecoder.zig
Normal file
350
src/bun.js/webcore/TextDecoder.zig
Normal file
@@ -0,0 +1,350 @@
|
||||
// used for utf8 decoding
|
||||
buffered: struct {
|
||||
buf: [3]u8 = .{0} ** 3,
|
||||
len: u2 = 0,
|
||||
|
||||
pub fn slice(this: *@This()) []const u8 {
|
||||
return this.buf[0..this.len];
|
||||
}
|
||||
} = .{},
|
||||
|
||||
// used for utf16 decoding
|
||||
lead_byte: ?u8 = null,
|
||||
lead_surrogate: ?u16 = null,
|
||||
|
||||
ignore_bom: bool = false,
|
||||
fatal: bool = false,
|
||||
encoding: EncodingLabel = EncodingLabel.@"UTF-8",
|
||||
|
||||
pub usingnamespace bun.New(TextDecoder);
|
||||
pub usingnamespace JSC.Codegen.JSTextDecoder;
|
||||
|
||||
pub fn finalize(this: *TextDecoder) void {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
pub fn getIgnoreBOM(
|
||||
this: *TextDecoder,
|
||||
_: *JSC.JSGlobalObject,
|
||||
) JSC.JSValue {
|
||||
return JSC.JSValue.jsBoolean(this.ignore_bom);
|
||||
}
|
||||
|
||||
pub fn getFatal(
|
||||
this: *TextDecoder,
|
||||
_: *JSC.JSGlobalObject,
|
||||
) JSC.JSValue {
|
||||
return JSC.JSValue.jsBoolean(this.fatal);
|
||||
}
|
||||
|
||||
pub fn getEncoding(
|
||||
this: *TextDecoder,
|
||||
globalThis: *JSC.JSGlobalObject,
|
||||
) JSC.JSValue {
|
||||
return ZigString.init(EncodingLabel.label.get(this.encoding).?).toJS(globalThis);
|
||||
}
|
||||
const Vector16 = std.meta.Vector(16, u16);
|
||||
const max_16_ascii: Vector16 = @splat(@as(u16, 127));
|
||||
|
||||
fn processCodeUnitUTF16(
|
||||
this: *TextDecoder,
|
||||
output: *std.ArrayListUnmanaged(u16),
|
||||
saw_error: *bool,
|
||||
code_unit: u16,
|
||||
) error{OutOfMemory}!void {
|
||||
if (this.lead_surrogate) |lead_surrogate| {
|
||||
this.lead_surrogate = null;
|
||||
|
||||
if (strings.u16IsTrail(code_unit)) {
|
||||
// TODO: why is this here?
|
||||
// const code_point = strings.u16GetSupplementary(lead_surrogate, code_unit);
|
||||
try output.appendSlice(
|
||||
bun.default_allocator,
|
||||
&.{ lead_surrogate, code_unit },
|
||||
);
|
||||
return;
|
||||
}
|
||||
try output.append(bun.default_allocator, strings.unicode_replacement);
|
||||
saw_error.* = true;
|
||||
}
|
||||
|
||||
if (strings.u16IsLead(code_unit)) {
|
||||
this.lead_surrogate = code_unit;
|
||||
return;
|
||||
}
|
||||
|
||||
if (strings.u16IsTrail(code_unit)) {
|
||||
try output.append(bun.default_allocator, strings.unicode_replacement);
|
||||
saw_error.* = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try output.append(bun.default_allocator, code_unit);
|
||||
return;
|
||||
}
|
||||
|
||||
pub fn codeUnitFromBytesUTF16(
|
||||
first: u16,
|
||||
second: u16,
|
||||
comptime big_endian: bool,
|
||||
) u16 {
|
||||
return if (comptime big_endian)
|
||||
(first << 8) | second
|
||||
else
|
||||
first | (second << 8);
|
||||
}
|
||||
|
||||
pub fn decodeUTF16(
|
||||
this: *TextDecoder,
|
||||
bytes: []const u8,
|
||||
comptime big_endian: bool,
|
||||
comptime flush: bool,
|
||||
) error{OutOfMemory}!struct { std.ArrayListUnmanaged(u16), bool } {
|
||||
var output: std.ArrayListUnmanaged(u16) = .{};
|
||||
try output.ensureTotalCapacity(bun.default_allocator, @divFloor(bytes.len, 2));
|
||||
|
||||
var remain = bytes;
|
||||
var saw_error = false;
|
||||
|
||||
if (this.lead_byte) |lead_byte| {
|
||||
if (remain.len > 0) {
|
||||
this.lead_byte = null;
|
||||
|
||||
try this.processCodeUnitUTF16(
|
||||
&output,
|
||||
&saw_error,
|
||||
codeUnitFromBytesUTF16(@intCast(lead_byte), @intCast(remain[0]), big_endian),
|
||||
);
|
||||
remain = remain[1..];
|
||||
}
|
||||
}
|
||||
|
||||
var i: usize = 0;
|
||||
|
||||
while (i < remain.len -| 1) {
|
||||
try this.processCodeUnitUTF16(
|
||||
&output,
|
||||
&saw_error,
|
||||
codeUnitFromBytesUTF16(@intCast(remain[i]), @intCast(remain[i + 1]), big_endian),
|
||||
);
|
||||
i += 2;
|
||||
}
|
||||
|
||||
if (remain.len != 0 and i == remain.len - 1) {
|
||||
this.lead_byte = remain[i];
|
||||
} else {
|
||||
bun.assertWithLocation(i == remain.len, @src());
|
||||
}
|
||||
|
||||
if (comptime flush) {
|
||||
if (this.lead_byte != null or this.lead_surrogate != null) {
|
||||
this.lead_byte = null;
|
||||
this.lead_surrogate = null;
|
||||
try output.append(bun.default_allocator, strings.unicode_replacement);
|
||||
saw_error = true;
|
||||
return .{ output, saw_error };
|
||||
}
|
||||
}
|
||||
|
||||
return .{ output, saw_error };
|
||||
}
|
||||
|
||||
pub fn decode(this: *TextDecoder, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue {
|
||||
const arguments = callframe.arguments_old(2).slice();
|
||||
|
||||
const input_slice = input_slice: {
|
||||
if (arguments.len == 0 or arguments[0].isUndefined()) {
|
||||
break :input_slice "";
|
||||
}
|
||||
|
||||
if (arguments[0].asArrayBuffer(globalThis)) |array_buffer| {
|
||||
break :input_slice array_buffer.slice();
|
||||
}
|
||||
|
||||
return globalThis.throwInvalidArguments("TextDecoder.decode expects an ArrayBuffer or TypedArray", .{});
|
||||
};
|
||||
|
||||
const stream = stream: {
|
||||
if (arguments.len > 1 and arguments[1].isObject()) {
|
||||
if (arguments[1].fastGet(globalThis, .stream)) |stream_value| {
|
||||
const stream_bool = stream_value.coerce(bool, globalThis);
|
||||
if (globalThis.hasException()) {
|
||||
return .zero;
|
||||
}
|
||||
break :stream stream_bool;
|
||||
}
|
||||
}
|
||||
|
||||
break :stream false;
|
||||
};
|
||||
|
||||
return switch (!stream) {
|
||||
inline else => |flush| this.decodeSlice(globalThis, input_slice, flush),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn decodeWithoutTypeChecks(this: *TextDecoder, globalThis: *JSC.JSGlobalObject, uint8array: *JSC.JSUint8Array) bun.JSError!JSValue {
|
||||
return this.decodeSlice(globalThis, uint8array.slice(), false);
|
||||
}
|
||||
|
||||
fn decodeSlice(this: *TextDecoder, globalThis: *JSC.JSGlobalObject, buffer_slice: []const u8, comptime flush: bool) bun.JSError!JSValue {
|
||||
switch (this.encoding) {
|
||||
EncodingLabel.latin1 => {
|
||||
if (strings.isAllASCII(buffer_slice)) {
|
||||
return ZigString.init(buffer_slice).toJS(globalThis);
|
||||
}
|
||||
|
||||
// It's unintuitive that we encode Latin1 as UTF16 even though the engine natively supports Latin1 strings...
|
||||
// However, this is also what WebKit seems to do.
|
||||
//
|
||||
// It's not clear why we couldn't jusst use Latin1 here, but tests failures proved it necessary.
|
||||
const out_length = strings.elementLengthLatin1IntoUTF16([]const u8, buffer_slice);
|
||||
const bytes = try globalThis.allocator().alloc(u16, out_length);
|
||||
|
||||
const out = strings.copyLatin1IntoUTF16([]u16, bytes, []const u8, buffer_slice);
|
||||
return ZigString.toExternalU16(bytes.ptr, out.written, globalThis);
|
||||
},
|
||||
EncodingLabel.@"UTF-8" => {
|
||||
const input, const deinit = input: {
|
||||
const maybe_without_bom = if (!this.ignore_bom and strings.hasPrefixComptime(buffer_slice, "\xef\xbb\xbf"))
|
||||
buffer_slice[3..]
|
||||
else
|
||||
buffer_slice;
|
||||
|
||||
if (this.buffered.len > 0) {
|
||||
defer this.buffered.len = 0;
|
||||
const joined = try bun.default_allocator.alloc(u8, maybe_without_bom.len + this.buffered.len);
|
||||
@memcpy(joined[0..this.buffered.len], this.buffered.slice());
|
||||
@memcpy(joined[this.buffered.len..][0..maybe_without_bom.len], maybe_without_bom);
|
||||
break :input .{ joined, true };
|
||||
}
|
||||
|
||||
break :input .{ maybe_without_bom, false };
|
||||
};
|
||||
|
||||
const maybe_decode_result = switch (this.fatal) {
|
||||
inline else => |fail_if_invalid| strings.toUTF16AllocMaybeBuffered(bun.default_allocator, input, fail_if_invalid, flush) catch |err| {
|
||||
if (deinit) bun.default_allocator.free(input);
|
||||
if (comptime fail_if_invalid) {
|
||||
if (err == error.InvalidByteSequence) {
|
||||
return globalThis.ERR_ENCODING_INVALID_ENCODED_DATA("Invalid byte sequence", .{}).throw();
|
||||
}
|
||||
}
|
||||
|
||||
bun.assert(err == error.OutOfMemory);
|
||||
return globalThis.throwOutOfMemory();
|
||||
},
|
||||
};
|
||||
|
||||
if (maybe_decode_result) |decode_result| {
|
||||
if (deinit) bun.default_allocator.free(input);
|
||||
const decoded, const leftover, const leftover_len = decode_result;
|
||||
bun.assert(this.buffered.len == 0);
|
||||
if (comptime !flush) {
|
||||
if (leftover_len != 0) {
|
||||
this.buffered.buf = leftover;
|
||||
this.buffered.len = leftover_len;
|
||||
}
|
||||
}
|
||||
return ZigString.toExternalU16(decoded.ptr, decoded.len, globalThis);
|
||||
}
|
||||
|
||||
bun.debugAssert(input.len == 0 or !deinit);
|
||||
|
||||
// Experiment: using mimalloc directly is slightly slower
|
||||
return ZigString.init(input).toJS(globalThis);
|
||||
},
|
||||
|
||||
inline .@"UTF-16LE", .@"UTF-16BE" => |utf16_encoding| {
|
||||
const bom = if (comptime utf16_encoding == .@"UTF-16LE") "\xff\xfe" else "\xfe\xff";
|
||||
const input = if (!this.ignore_bom and strings.hasPrefixComptime(buffer_slice, bom))
|
||||
buffer_slice[2..]
|
||||
else
|
||||
buffer_slice;
|
||||
|
||||
var decoded, const saw_error = try this.decodeUTF16(input, utf16_encoding == .@"UTF-16BE", flush);
|
||||
|
||||
if (saw_error and this.fatal) {
|
||||
decoded.deinit(bun.default_allocator);
|
||||
return globalThis.ERR_ENCODING_INVALID_ENCODED_DATA("The encoded data was not valid {s} data", .{@tagName(utf16_encoding)}).throw();
|
||||
}
|
||||
|
||||
var output = bun.String.fromUTF16(decoded.items);
|
||||
return output.toJS(globalThis);
|
||||
},
|
||||
else => {
|
||||
return globalThis.throwInvalidArguments("TextDecoder.decode set to unsupported encoding", .{});
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn constructor(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!*TextDecoder {
|
||||
var args_ = callframe.arguments_old(2);
|
||||
var arguments: []const JSC.JSValue = args_.ptr[0..args_.len];
|
||||
|
||||
var decoder = TextDecoder{};
|
||||
|
||||
if (arguments.len > 0) {
|
||||
// encoding
|
||||
if (arguments[0].isString()) {
|
||||
var str = try arguments[0].toSlice(globalThis, bun.default_allocator);
|
||||
defer if (str.isAllocated()) str.deinit();
|
||||
|
||||
if (EncodingLabel.which(str.slice())) |label| {
|
||||
decoder.encoding = label;
|
||||
} else {
|
||||
return globalThis.throwInvalidArguments("Unsupported encoding label \"{s}\"", .{str.slice()});
|
||||
}
|
||||
} else if (arguments[0].isUndefined()) {
|
||||
// default to utf-8
|
||||
decoder.encoding = EncodingLabel.@"UTF-8";
|
||||
} else {
|
||||
return globalThis.throwInvalidArguments("TextDecoder(encoding) label is invalid", .{});
|
||||
}
|
||||
|
||||
if (arguments.len >= 2) {
|
||||
const options = arguments[1];
|
||||
|
||||
if (!options.isObject()) {
|
||||
return globalThis.throwInvalidArguments("TextDecoder(options) is invalid", .{});
|
||||
}
|
||||
|
||||
if (try options.get(globalThis, "fatal")) |fatal| {
|
||||
if (fatal.isBoolean()) {
|
||||
decoder.fatal = fatal.asBoolean();
|
||||
} else {
|
||||
return globalThis.throwInvalidArguments("TextDecoder(options) fatal is invalid. Expected boolean value", .{});
|
||||
}
|
||||
}
|
||||
|
||||
if (try options.get(globalThis, "ignoreBOM")) |ignoreBOM| {
|
||||
if (ignoreBOM.isBoolean()) {
|
||||
decoder.ignore_bom = ignoreBOM.asBoolean();
|
||||
} else {
|
||||
return globalThis.throwInvalidArguments("TextDecoder(options) ignoreBOM is invalid. Expected boolean value", .{});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return TextDecoder.new(decoder);
|
||||
}
|
||||
|
||||
const TextDecoder = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const bun = @import("root").bun;
|
||||
const JSC = bun.JSC;
|
||||
const Output = bun.Output;
|
||||
const MutableString = bun.MutableString;
|
||||
const strings = bun.strings;
|
||||
const string = bun.string;
|
||||
const FeatureFlags = bun.FeatureFlags;
|
||||
const ArrayBuffer = JSC.ArrayBuffer;
|
||||
const JSUint8Array = JSC.JSUint8Array;
|
||||
const ZigString = JSC.ZigString;
|
||||
const JSInternalPromise = JSC.JSInternalPromise;
|
||||
const JSPromise = JSC.JSPromise;
|
||||
const JSValue = JSC.JSValue;
|
||||
const JSGlobalObject = JSC.JSGlobalObject;
|
||||
const EncodingLabel = JSC.WebCore.EncodingLabel;
|
||||
255
src/bun.js/webcore/TextEncoder.zig
Normal file
255
src/bun.js/webcore/TextEncoder.zig
Normal file
@@ -0,0 +1,255 @@
|
||||
pub export fn TextEncoder__encode8(
|
||||
globalThis: *JSGlobalObject,
|
||||
ptr: [*]const u8,
|
||||
len: usize,
|
||||
) JSValue {
|
||||
// as much as possible, rely on JSC to own the memory
|
||||
// their code is more battle-tested than bun's code
|
||||
// so we do a stack allocation here
|
||||
// and then copy into JSC memory
|
||||
// unless it's huge
|
||||
// JSC will GC Uint8Array that occupy less than 512 bytes
|
||||
// so it's extra good for that case
|
||||
// this also means there won't be reallocations for small strings
|
||||
var buf: [2048]u8 = undefined;
|
||||
const slice = ptr[0..len];
|
||||
|
||||
if (slice.len <= buf.len / 2) {
|
||||
const result = strings.copyLatin1IntoUTF8(&buf, []const u8, slice);
|
||||
const uint8array = JSC.JSValue.createUninitializedUint8Array(globalThis, result.written);
|
||||
bun.assert(result.written <= buf.len);
|
||||
bun.assert(result.read == slice.len);
|
||||
const array_buffer = uint8array.asArrayBuffer(globalThis) orelse return .zero;
|
||||
bun.assert(result.written == array_buffer.len);
|
||||
@memcpy(array_buffer.byteSlice()[0..result.written], buf[0..result.written]);
|
||||
return uint8array;
|
||||
} else {
|
||||
const bytes = strings.allocateLatin1IntoUTF8(globalThis.bunVM().allocator, []const u8, slice) catch {
|
||||
return globalThis.throwOutOfMemoryValue();
|
||||
};
|
||||
bun.assert(bytes.len >= slice.len);
|
||||
return ArrayBuffer.fromBytes(bytes, .Uint8Array).toJSUnchecked(globalThis, null);
|
||||
}
|
||||
}
|
||||
pub export fn TextEncoder__encode16(
|
||||
globalThis: *JSGlobalObject,
|
||||
ptr: [*]const u16,
|
||||
len: usize,
|
||||
) JSValue {
|
||||
// as much as possible, rely on JSC to own the memory
|
||||
// their code is more battle-tested than bun's code
|
||||
// so we do a stack allocation here
|
||||
// and then copy into JSC memory
|
||||
// unless it's huge
|
||||
// JSC will GC Uint8Array that occupy less than 512 bytes
|
||||
// so it's extra good for that case
|
||||
// this also means there won't be reallocations for small strings
|
||||
var buf: [2048]u8 = undefined;
|
||||
|
||||
const slice = ptr[0..len];
|
||||
|
||||
// max utf16 -> utf8 length
|
||||
if (slice.len <= buf.len / 4) {
|
||||
const result = strings.copyUTF16IntoUTF8(&buf, @TypeOf(slice), slice, true);
|
||||
if (result.read == 0 or result.written == 0) {
|
||||
const uint8array = JSC.JSValue.createUninitializedUint8Array(globalThis, 3);
|
||||
const array_buffer = uint8array.asArrayBuffer(globalThis).?;
|
||||
const replacement_char = [_]u8{ 239, 191, 189 };
|
||||
@memcpy(array_buffer.slice()[0..replacement_char.len], &replacement_char);
|
||||
return uint8array;
|
||||
}
|
||||
const uint8array = JSC.JSValue.createUninitializedUint8Array(globalThis, result.written);
|
||||
bun.assert(result.written <= buf.len);
|
||||
bun.assert(result.read == slice.len);
|
||||
const array_buffer = uint8array.asArrayBuffer(globalThis).?;
|
||||
bun.assert(result.written == array_buffer.len);
|
||||
@memcpy(array_buffer.slice()[0..result.written], buf[0..result.written]);
|
||||
return uint8array;
|
||||
} else {
|
||||
const bytes = strings.toUTF8AllocWithType(
|
||||
bun.default_allocator,
|
||||
@TypeOf(slice),
|
||||
slice,
|
||||
) catch {
|
||||
return JSC.toInvalidArguments("Out of memory", .{}, globalThis);
|
||||
};
|
||||
return ArrayBuffer.fromBytes(bytes, .Uint8Array).toJSUnchecked(globalThis, null);
|
||||
}
|
||||
}
|
||||
|
||||
pub export fn c(
|
||||
globalThis: *JSGlobalObject,
|
||||
ptr: [*]const u16,
|
||||
len: usize,
|
||||
) JSValue {
|
||||
// as much as possible, rely on JSC to own the memory
|
||||
// their code is more battle-tested than bun's code
|
||||
// so we do a stack allocation here
|
||||
// and then copy into JSC memory
|
||||
// unless it's huge
|
||||
// JSC will GC Uint8Array that occupy less than 512 bytes
|
||||
// so it's extra good for that case
|
||||
// this also means there won't be reallocations for small strings
|
||||
var buf: [2048]u8 = undefined;
|
||||
|
||||
const slice = ptr[0..len];
|
||||
|
||||
// max utf16 -> utf8 length
|
||||
if (slice.len <= buf.len / 4) {
|
||||
const result = strings.copyUTF16IntoUTF8(&buf, @TypeOf(slice), slice, true);
|
||||
if (result.read == 0 or result.written == 0) {
|
||||
const uint8array = JSC.JSValue.createUninitializedUint8Array(globalThis, 3);
|
||||
const array_buffer = uint8array.asArrayBuffer(globalThis).?;
|
||||
const replacement_char = [_]u8{ 239, 191, 189 };
|
||||
@memcpy(array_buffer.slice()[0..replacement_char.len], &replacement_char);
|
||||
return uint8array;
|
||||
}
|
||||
const uint8array = JSC.JSValue.createUninitializedUint8Array(globalThis, result.written);
|
||||
bun.assert(result.written <= buf.len);
|
||||
bun.assert(result.read == slice.len);
|
||||
const array_buffer = uint8array.asArrayBuffer(globalThis).?;
|
||||
bun.assert(result.written == array_buffer.len);
|
||||
@memcpy(array_buffer.slice()[0..result.written], buf[0..result.written]);
|
||||
return uint8array;
|
||||
} else {
|
||||
const bytes = strings.toUTF8AllocWithType(
|
||||
bun.default_allocator,
|
||||
@TypeOf(slice),
|
||||
slice,
|
||||
) catch {
|
||||
return globalThis.throwOutOfMemoryValue();
|
||||
};
|
||||
return ArrayBuffer.fromBytes(bytes, .Uint8Array).toJSUnchecked(globalThis, null);
|
||||
}
|
||||
}
|
||||
|
||||
// This is a fast path for copying a Rope string into a Uint8Array.
|
||||
// This keeps us from an extra string temporary allocation
|
||||
const RopeStringEncoder = struct {
|
||||
globalThis: *JSGlobalObject,
|
||||
buf: []u8,
|
||||
tail: usize = 0,
|
||||
any_non_ascii: bool = false,
|
||||
|
||||
pub fn append8(it: *JSC.JSString.Iterator, ptr: [*]const u8, len: u32) callconv(.C) void {
|
||||
var this = bun.cast(*RopeStringEncoder, it.data.?);
|
||||
const result = strings.copyLatin1IntoUTF8StopOnNonASCII(this.buf[this.tail..], []const u8, ptr[0..len], true);
|
||||
if (result.read == std.math.maxInt(u32) and result.written == std.math.maxInt(u32)) {
|
||||
it.stop = 1;
|
||||
this.any_non_ascii = true;
|
||||
} else {
|
||||
this.tail += result.written;
|
||||
}
|
||||
}
|
||||
pub fn append16(it: *JSC.JSString.Iterator, _: [*]const u16, _: u32) callconv(.C) void {
|
||||
var this = bun.cast(*RopeStringEncoder, it.data.?);
|
||||
this.any_non_ascii = true;
|
||||
it.stop = 1;
|
||||
}
|
||||
pub fn write8(it: *JSC.JSString.Iterator, ptr: [*]const u8, len: u32, offset: u32) callconv(.C) void {
|
||||
var this = bun.cast(*RopeStringEncoder, it.data.?);
|
||||
const result = strings.copyLatin1IntoUTF8StopOnNonASCII(this.buf[offset..], []const u8, ptr[0..len], true);
|
||||
if (result.read == std.math.maxInt(u32) and result.written == std.math.maxInt(u32)) {
|
||||
it.stop = 1;
|
||||
this.any_non_ascii = true;
|
||||
}
|
||||
}
|
||||
pub fn write16(it: *JSC.JSString.Iterator, _: [*]const u16, _: u32, _: u32) callconv(.C) void {
|
||||
var this = bun.cast(*RopeStringEncoder, it.data.?);
|
||||
this.any_non_ascii = true;
|
||||
it.stop = 1;
|
||||
}
|
||||
|
||||
pub fn iter(this: *RopeStringEncoder) JSC.JSString.Iterator {
|
||||
return .{
|
||||
.data = this,
|
||||
.stop = 0,
|
||||
.append8 = append8,
|
||||
.append16 = append16,
|
||||
.write8 = write8,
|
||||
.write16 = write16,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// This fast path is only suitable for ASCII strings
|
||||
// It's not suitable for UTF-16 strings, because getting the byteLength is unpredictable
|
||||
// It also isn't usable for latin1 strings which contain non-ascii characters
|
||||
pub export fn TextEncoder__encodeRopeString(
|
||||
globalThis: *JSGlobalObject,
|
||||
rope_str: *JSC.JSString,
|
||||
) JSValue {
|
||||
if (comptime Environment.allow_assert) bun.assert(rope_str.is8Bit());
|
||||
var stack_buf: [2048]u8 = undefined;
|
||||
var buf_to_use: []u8 = &stack_buf;
|
||||
const length = rope_str.length();
|
||||
var array: JSValue = .zero;
|
||||
if (length > stack_buf.len / 2) {
|
||||
array = JSC.JSValue.createUninitializedUint8Array(globalThis, length);
|
||||
array.ensureStillAlive();
|
||||
buf_to_use = array.asArrayBuffer(globalThis).?.slice();
|
||||
}
|
||||
var encoder = RopeStringEncoder{
|
||||
.globalThis = globalThis,
|
||||
.buf = buf_to_use,
|
||||
};
|
||||
var iter = encoder.iter();
|
||||
array.ensureStillAlive();
|
||||
rope_str.iterator(globalThis, &iter);
|
||||
array.ensureStillAlive();
|
||||
|
||||
if (encoder.any_non_ascii) {
|
||||
return .undefined;
|
||||
}
|
||||
|
||||
if (array == .zero) {
|
||||
array = JSC.JSValue.createUninitializedUint8Array(globalThis, length);
|
||||
array.ensureStillAlive();
|
||||
@memcpy(array.asArrayBuffer(globalThis).?.ptr[0..length], buf_to_use[0..length]);
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
pub export fn TextEncoder__encodeInto16(
|
||||
input_ptr: [*]const u16,
|
||||
input_len: usize,
|
||||
buf_ptr: [*]u8,
|
||||
buf_len: usize,
|
||||
) u64 {
|
||||
const output = buf_ptr[0..buf_len];
|
||||
const input = input_ptr[0..input_len];
|
||||
var result: strings.EncodeIntoResult = strings.copyUTF16IntoUTF8(output, []const u16, input, false);
|
||||
if (output.len >= 3 and (result.read == 0 or result.written == 0)) {
|
||||
const replacement_char = [_]u8{ 239, 191, 189 };
|
||||
@memcpy(buf_ptr[0..replacement_char.len], &replacement_char);
|
||||
result.read = 1;
|
||||
result.written = 3;
|
||||
}
|
||||
const sized: [2]u32 = .{ result.read, result.written };
|
||||
return @bitCast(sized);
|
||||
}
|
||||
|
||||
pub export fn TextEncoder__encodeInto8(
|
||||
input_ptr: [*]const u8,
|
||||
input_len: usize,
|
||||
buf_ptr: [*]u8,
|
||||
buf_len: usize,
|
||||
) u64 {
|
||||
const output = buf_ptr[0..buf_len];
|
||||
const input = input_ptr[0..input_len];
|
||||
const result: strings.EncodeIntoResult =
|
||||
strings.copyLatin1IntoUTF8(output, []const u8, input);
|
||||
const sized: [2]u32 = .{ result.read, result.written };
|
||||
return @bitCast(sized);
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
const bun = @import("root").bun;
|
||||
const strings = bun.strings;
|
||||
const JSC = bun.JSC;
|
||||
const Environment = bun.Environment;
|
||||
const JSGlobalObject = JSC.JSGlobalObject;
|
||||
const JSValue = JSC.JSValue;
|
||||
const ArrayBuffer = JSC.ArrayBuffer;
|
||||
const TextEncoder = @This();
|
||||
213
src/bun.js/webcore/TextEncoderStreamEncoder.zig
Normal file
213
src/bun.js/webcore/TextEncoderStreamEncoder.zig
Normal file
@@ -0,0 +1,213 @@
|
||||
pending_lead_surrogate: ?u16 = null,
|
||||
|
||||
const log = Output.scoped(.TextEncoderStreamEncoder, false);
|
||||
|
||||
pub usingnamespace JSC.Codegen.JSTextEncoderStreamEncoder;
|
||||
pub usingnamespace bun.New(TextEncoderStreamEncoder);
|
||||
|
||||
pub fn finalize(this: *TextEncoderStreamEncoder) void {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
pub fn constructor(_: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!*TextEncoderStreamEncoder {
|
||||
return TextEncoderStreamEncoder.new(.{});
|
||||
}
|
||||
|
||||
pub fn encode(this: *TextEncoderStreamEncoder, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue {
|
||||
const arguments = callFrame.arguments_old(1).slice();
|
||||
if (arguments.len == 0) {
|
||||
return globalObject.throwNotEnoughArguments("TextEncoderStreamEncoder.encode", 1, arguments.len);
|
||||
}
|
||||
|
||||
const str = try arguments[0].getZigString(globalObject);
|
||||
|
||||
if (str.is16Bit()) {
|
||||
return this.encodeUTF16(globalObject, str.utf16SliceAligned());
|
||||
}
|
||||
|
||||
return this.encodeLatin1(globalObject, str.slice());
|
||||
}
|
||||
|
||||
pub fn encodeWithoutTypeChecks(this: *TextEncoderStreamEncoder, globalObject: *JSC.JSGlobalObject, input: *JSC.JSString) JSValue {
|
||||
const str = input.getZigString(globalObject);
|
||||
|
||||
if (str.is16Bit()) {
|
||||
return this.encodeUTF16(globalObject, str.utf16SliceAligned());
|
||||
}
|
||||
|
||||
return this.encodeLatin1(globalObject, str.slice());
|
||||
}
|
||||
|
||||
fn encodeLatin1(this: *TextEncoderStreamEncoder, globalObject: *JSGlobalObject, input: []const u8) JSValue {
|
||||
log("encodeLatin1: \"{s}\"", .{input});
|
||||
|
||||
if (input.len == 0) return JSUint8Array.createEmpty(globalObject);
|
||||
|
||||
const prepend_replacement_len: usize = prepend_replacement: {
|
||||
if (this.pending_lead_surrogate != null) {
|
||||
this.pending_lead_surrogate = null;
|
||||
// no latin1 surrogate pairs
|
||||
break :prepend_replacement 3;
|
||||
}
|
||||
|
||||
break :prepend_replacement 0;
|
||||
};
|
||||
// In a previous benchmark, counting the length took about as much time as allocating the buffer.
|
||||
//
|
||||
// Benchmark Time % CPU (ns) Iterations Ratio
|
||||
// 288.00 ms 13.5% 288.00 ms simdutf::arm64::implementation::convert_latin1_to_utf8(char const*, unsigned long, char*) const
|
||||
// 278.00 ms 13.0% 278.00 ms simdutf::arm64::implementation::utf8_length_from_latin1(char const*, unsigned long) const
|
||||
//
|
||||
//
|
||||
var buffer = std.ArrayList(u8).initCapacity(bun.default_allocator, input.len + prepend_replacement_len) catch {
|
||||
return globalObject.throwOutOfMemoryValue();
|
||||
};
|
||||
if (prepend_replacement_len > 0) {
|
||||
buffer.appendSliceAssumeCapacity(&[3]u8{ 0xef, 0xbf, 0xbd });
|
||||
}
|
||||
|
||||
var remain = input;
|
||||
while (remain.len > 0) {
|
||||
const result = strings.copyLatin1IntoUTF8(buffer.unusedCapacitySlice(), []const u8, remain);
|
||||
|
||||
buffer.items.len += result.written;
|
||||
remain = remain[result.read..];
|
||||
|
||||
if (result.written == 0 and result.read == 0) {
|
||||
buffer.ensureUnusedCapacity(2) catch {
|
||||
buffer.deinit();
|
||||
return globalObject.throwOutOfMemoryValue();
|
||||
};
|
||||
} else if (buffer.items.len == buffer.capacity and remain.len > 0) {
|
||||
buffer.ensureTotalCapacity(buffer.items.len + remain.len + 1) catch {
|
||||
buffer.deinit();
|
||||
return globalObject.throwOutOfMemoryValue();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (comptime Environment.isDebug) {
|
||||
// wrap in comptime if so simdutf isn't called in a release build here.
|
||||
bun.debugAssert(buffer.items.len == (bun.simdutf.length.utf8.from.latin1(input) + prepend_replacement_len));
|
||||
}
|
||||
|
||||
return JSC.JSUint8Array.fromBytes(globalObject, buffer.items);
|
||||
}
|
||||
|
||||
fn encodeUTF16(this: *TextEncoderStreamEncoder, globalObject: *JSGlobalObject, input: []const u16) JSValue {
|
||||
log("encodeUTF16: \"{}\"", .{bun.fmt.utf16(input)});
|
||||
|
||||
if (input.len == 0) return JSUint8Array.createEmpty(globalObject);
|
||||
|
||||
const Prepend = struct {
|
||||
bytes: [4]u8,
|
||||
len: u3,
|
||||
|
||||
pub const replacement: @This() = .{ .bytes = .{ 0xef, 0xbf, 0xbd, 0 }, .len = 3 };
|
||||
|
||||
pub fn fromSequence(seq: [4]u8, length: u3) @This() {
|
||||
return .{ .bytes = seq, .len = length };
|
||||
}
|
||||
};
|
||||
|
||||
var remain = input;
|
||||
|
||||
const prepend: ?Prepend = prepend: {
|
||||
if (this.pending_lead_surrogate) |lead| {
|
||||
this.pending_lead_surrogate = null;
|
||||
const maybe_trail = remain[0];
|
||||
if (strings.u16IsTrail(maybe_trail)) {
|
||||
const converted = strings.utf16CodepointWithFFFD([]const u16, &.{ lead, maybe_trail });
|
||||
// shouldn't fail because `u16IsTrail` is true and `pending_lead_surrogate` is always
|
||||
// a valid lead.
|
||||
bun.debugAssert(!converted.fail);
|
||||
|
||||
const sequence = strings.wtf8Sequence(converted.code_point);
|
||||
|
||||
remain = remain[1..];
|
||||
if (remain.len == 0) {
|
||||
return JSUint8Array.fromBytesCopy(
|
||||
globalObject,
|
||||
sequence[0..converted.utf8Width()],
|
||||
);
|
||||
}
|
||||
|
||||
break :prepend Prepend.fromSequence(sequence, converted.utf8Width());
|
||||
}
|
||||
|
||||
break :prepend Prepend.replacement;
|
||||
}
|
||||
break :prepend null;
|
||||
};
|
||||
|
||||
const length = bun.simdutf.length.utf8.from.utf16.le(remain);
|
||||
|
||||
var buf = std.ArrayList(u8).initCapacity(
|
||||
bun.default_allocator,
|
||||
length + @as(usize, if (prepend) |pre| pre.len else 0),
|
||||
) catch {
|
||||
return globalObject.throwOutOfMemoryValue();
|
||||
};
|
||||
|
||||
if (prepend) |*pre| {
|
||||
buf.appendSliceAssumeCapacity(pre.bytes[0..pre.len]);
|
||||
}
|
||||
|
||||
const result = bun.simdutf.convert.utf16.to.utf8.with_errors.le(remain, buf.unusedCapacitySlice());
|
||||
|
||||
switch (result.status) {
|
||||
else => {
|
||||
// Slow path: there was invalid UTF-16, so we need to convert it without simdutf.
|
||||
const lead_surrogate = strings.toUTF8ListWithTypeBun(&buf, []const u16, remain, true) catch {
|
||||
buf.deinit();
|
||||
return globalObject.throwOutOfMemoryValue();
|
||||
};
|
||||
|
||||
if (lead_surrogate) |pending_lead| {
|
||||
this.pending_lead_surrogate = pending_lead;
|
||||
if (buf.items.len == 0) return JSUint8Array.createEmpty(globalObject);
|
||||
}
|
||||
|
||||
return JSC.JSUint8Array.fromBytes(globalObject, buf.items);
|
||||
},
|
||||
.success => {
|
||||
buf.items.len += result.count;
|
||||
return JSC.JSUint8Array.fromBytes(globalObject, buf.items);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flush(this: *TextEncoderStreamEncoder, globalObject: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue {
|
||||
return flushBody(this, globalObject);
|
||||
}
|
||||
|
||||
pub fn flushWithoutTypeChecks(this: *TextEncoderStreamEncoder, globalObject: *JSGlobalObject) JSValue {
|
||||
return flushBody(this, globalObject);
|
||||
}
|
||||
|
||||
fn flushBody(this: *TextEncoderStreamEncoder, globalObject: *JSGlobalObject) JSValue {
|
||||
return if (this.pending_lead_surrogate == null)
|
||||
JSUint8Array.createEmpty(globalObject)
|
||||
else
|
||||
JSUint8Array.fromBytesCopy(globalObject, &.{ 0xef, 0xbf, 0xbd });
|
||||
}
|
||||
|
||||
const TextEncoderStreamEncoder = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const bun = @import("root").bun;
|
||||
const JSC = bun.JSC;
|
||||
const Output = bun.Output;
|
||||
const MutableString = bun.MutableString;
|
||||
const strings = bun.strings;
|
||||
const string = bun.string;
|
||||
const FeatureFlags = bun.FeatureFlags;
|
||||
const ArrayBuffer = JSC.ArrayBuffer;
|
||||
const JSUint8Array = JSC.JSUint8Array;
|
||||
const ZigString = JSC.ZigString;
|
||||
const JSInternalPromise = JSC.JSInternalPromise;
|
||||
const JSPromise = JSC.JSPromise;
|
||||
const JSValue = JSC.JSValue;
|
||||
const JSGlobalObject = JSC.JSGlobalObject;
|
||||
const EncodingLabel = JSC.WebCore.EncodingLabel;
|
||||
const Environment = bun.Environment;
|
||||
@@ -35,949 +35,10 @@ const Task = JSC.Task;
|
||||
|
||||
const picohttp = bun.picohttp;
|
||||
|
||||
pub const TextEncoder = struct {
|
||||
pub export fn TextEncoder__encode8(
|
||||
globalThis: *JSGlobalObject,
|
||||
ptr: [*]const u8,
|
||||
len: usize,
|
||||
) JSValue {
|
||||
// as much as possible, rely on JSC to own the memory
|
||||
// their code is more battle-tested than bun's code
|
||||
// so we do a stack allocation here
|
||||
// and then copy into JSC memory
|
||||
// unless it's huge
|
||||
// JSC will GC Uint8Array that occupy less than 512 bytes
|
||||
// so it's extra good for that case
|
||||
// this also means there won't be reallocations for small strings
|
||||
var buf: [2048]u8 = undefined;
|
||||
const slice = ptr[0..len];
|
||||
|
||||
if (slice.len <= buf.len / 2) {
|
||||
const result = strings.copyLatin1IntoUTF8(&buf, []const u8, slice);
|
||||
const uint8array = JSC.JSValue.createUninitializedUint8Array(globalThis, result.written);
|
||||
bun.assert(result.written <= buf.len);
|
||||
bun.assert(result.read == slice.len);
|
||||
const array_buffer = uint8array.asArrayBuffer(globalThis) orelse return .zero;
|
||||
bun.assert(result.written == array_buffer.len);
|
||||
@memcpy(array_buffer.byteSlice()[0..result.written], buf[0..result.written]);
|
||||
return uint8array;
|
||||
} else {
|
||||
const bytes = strings.allocateLatin1IntoUTF8(globalThis.bunVM().allocator, []const u8, slice) catch {
|
||||
return globalThis.throwOutOfMemoryValue();
|
||||
};
|
||||
bun.assert(bytes.len >= slice.len);
|
||||
return ArrayBuffer.fromBytes(bytes, .Uint8Array).toJSUnchecked(globalThis, null);
|
||||
}
|
||||
}
|
||||
pub export fn TextEncoder__encode16(
|
||||
globalThis: *JSGlobalObject,
|
||||
ptr: [*]const u16,
|
||||
len: usize,
|
||||
) JSValue {
|
||||
// as much as possible, rely on JSC to own the memory
|
||||
// their code is more battle-tested than bun's code
|
||||
// so we do a stack allocation here
|
||||
// and then copy into JSC memory
|
||||
// unless it's huge
|
||||
// JSC will GC Uint8Array that occupy less than 512 bytes
|
||||
// so it's extra good for that case
|
||||
// this also means there won't be reallocations for small strings
|
||||
var buf: [2048]u8 = undefined;
|
||||
|
||||
const slice = ptr[0..len];
|
||||
|
||||
// max utf16 -> utf8 length
|
||||
if (slice.len <= buf.len / 4) {
|
||||
const result = strings.copyUTF16IntoUTF8(&buf, @TypeOf(slice), slice, true);
|
||||
if (result.read == 0 or result.written == 0) {
|
||||
const uint8array = JSC.JSValue.createUninitializedUint8Array(globalThis, 3);
|
||||
const array_buffer = uint8array.asArrayBuffer(globalThis).?;
|
||||
const replacement_char = [_]u8{ 239, 191, 189 };
|
||||
@memcpy(array_buffer.slice()[0..replacement_char.len], &replacement_char);
|
||||
return uint8array;
|
||||
}
|
||||
const uint8array = JSC.JSValue.createUninitializedUint8Array(globalThis, result.written);
|
||||
bun.assert(result.written <= buf.len);
|
||||
bun.assert(result.read == slice.len);
|
||||
const array_buffer = uint8array.asArrayBuffer(globalThis).?;
|
||||
bun.assert(result.written == array_buffer.len);
|
||||
@memcpy(array_buffer.slice()[0..result.written], buf[0..result.written]);
|
||||
return uint8array;
|
||||
} else {
|
||||
const bytes = strings.toUTF8AllocWithType(
|
||||
bun.default_allocator,
|
||||
@TypeOf(slice),
|
||||
slice,
|
||||
) catch {
|
||||
return JSC.toInvalidArguments("Out of memory", .{}, globalThis);
|
||||
};
|
||||
return ArrayBuffer.fromBytes(bytes, .Uint8Array).toJSUnchecked(globalThis, null);
|
||||
}
|
||||
}
|
||||
|
||||
pub export fn c(
|
||||
globalThis: *JSGlobalObject,
|
||||
ptr: [*]const u16,
|
||||
len: usize,
|
||||
) JSValue {
|
||||
// as much as possible, rely on JSC to own the memory
|
||||
// their code is more battle-tested than bun's code
|
||||
// so we do a stack allocation here
|
||||
// and then copy into JSC memory
|
||||
// unless it's huge
|
||||
// JSC will GC Uint8Array that occupy less than 512 bytes
|
||||
// so it's extra good for that case
|
||||
// this also means there won't be reallocations for small strings
|
||||
var buf: [2048]u8 = undefined;
|
||||
|
||||
const slice = ptr[0..len];
|
||||
|
||||
// max utf16 -> utf8 length
|
||||
if (slice.len <= buf.len / 4) {
|
||||
const result = strings.copyUTF16IntoUTF8(&buf, @TypeOf(slice), slice, true);
|
||||
if (result.read == 0 or result.written == 0) {
|
||||
const uint8array = JSC.JSValue.createUninitializedUint8Array(globalThis, 3);
|
||||
const array_buffer = uint8array.asArrayBuffer(globalThis).?;
|
||||
const replacement_char = [_]u8{ 239, 191, 189 };
|
||||
@memcpy(array_buffer.slice()[0..replacement_char.len], &replacement_char);
|
||||
return uint8array;
|
||||
}
|
||||
const uint8array = JSC.JSValue.createUninitializedUint8Array(globalThis, result.written);
|
||||
bun.assert(result.written <= buf.len);
|
||||
bun.assert(result.read == slice.len);
|
||||
const array_buffer = uint8array.asArrayBuffer(globalThis).?;
|
||||
bun.assert(result.written == array_buffer.len);
|
||||
@memcpy(array_buffer.slice()[0..result.written], buf[0..result.written]);
|
||||
return uint8array;
|
||||
} else {
|
||||
const bytes = strings.toUTF8AllocWithType(
|
||||
bun.default_allocator,
|
||||
@TypeOf(slice),
|
||||
slice,
|
||||
) catch {
|
||||
return globalThis.throwOutOfMemoryValue();
|
||||
};
|
||||
return ArrayBuffer.fromBytes(bytes, .Uint8Array).toJSUnchecked(globalThis, null);
|
||||
}
|
||||
}
|
||||
|
||||
// This is a fast path for copying a Rope string into a Uint8Array.
|
||||
// This keeps us from an extra string temporary allocation
|
||||
const RopeStringEncoder = struct {
|
||||
globalThis: *JSGlobalObject,
|
||||
buf: []u8,
|
||||
tail: usize = 0,
|
||||
any_non_ascii: bool = false,
|
||||
|
||||
pub fn append8(it: *JSC.JSString.Iterator, ptr: [*]const u8, len: u32) callconv(.C) void {
|
||||
var this = bun.cast(*RopeStringEncoder, it.data.?);
|
||||
const result = strings.copyLatin1IntoUTF8StopOnNonASCII(this.buf[this.tail..], []const u8, ptr[0..len], true);
|
||||
if (result.read == std.math.maxInt(u32) and result.written == std.math.maxInt(u32)) {
|
||||
it.stop = 1;
|
||||
this.any_non_ascii = true;
|
||||
} else {
|
||||
this.tail += result.written;
|
||||
}
|
||||
}
|
||||
pub fn append16(it: *JSC.JSString.Iterator, _: [*]const u16, _: u32) callconv(.C) void {
|
||||
var this = bun.cast(*RopeStringEncoder, it.data.?);
|
||||
this.any_non_ascii = true;
|
||||
it.stop = 1;
|
||||
}
|
||||
pub fn write8(it: *JSC.JSString.Iterator, ptr: [*]const u8, len: u32, offset: u32) callconv(.C) void {
|
||||
var this = bun.cast(*RopeStringEncoder, it.data.?);
|
||||
const result = strings.copyLatin1IntoUTF8StopOnNonASCII(this.buf[offset..], []const u8, ptr[0..len], true);
|
||||
if (result.read == std.math.maxInt(u32) and result.written == std.math.maxInt(u32)) {
|
||||
it.stop = 1;
|
||||
this.any_non_ascii = true;
|
||||
}
|
||||
}
|
||||
pub fn write16(it: *JSC.JSString.Iterator, _: [*]const u16, _: u32, _: u32) callconv(.C) void {
|
||||
var this = bun.cast(*RopeStringEncoder, it.data.?);
|
||||
this.any_non_ascii = true;
|
||||
it.stop = 1;
|
||||
}
|
||||
|
||||
pub fn iter(this: *RopeStringEncoder) JSC.JSString.Iterator {
|
||||
return .{
|
||||
.data = this,
|
||||
.stop = 0,
|
||||
.append8 = append8,
|
||||
.append16 = append16,
|
||||
.write8 = write8,
|
||||
.write16 = write16,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// This fast path is only suitable for ASCII strings
|
||||
// It's not suitable for UTF-16 strings, because getting the byteLength is unpredictable
|
||||
// It also isn't usable for latin1 strings which contain non-ascii characters
|
||||
pub export fn TextEncoder__encodeRopeString(
|
||||
globalThis: *JSGlobalObject,
|
||||
rope_str: *JSC.JSString,
|
||||
) JSValue {
|
||||
if (comptime Environment.allow_assert) bun.assert(rope_str.is8Bit());
|
||||
var stack_buf: [2048]u8 = undefined;
|
||||
var buf_to_use: []u8 = &stack_buf;
|
||||
const length = rope_str.length();
|
||||
var array: JSValue = .zero;
|
||||
if (length > stack_buf.len / 2) {
|
||||
array = JSC.JSValue.createUninitializedUint8Array(globalThis, length);
|
||||
array.ensureStillAlive();
|
||||
buf_to_use = array.asArrayBuffer(globalThis).?.slice();
|
||||
}
|
||||
var encoder = RopeStringEncoder{
|
||||
.globalThis = globalThis,
|
||||
.buf = buf_to_use,
|
||||
};
|
||||
var iter = encoder.iter();
|
||||
array.ensureStillAlive();
|
||||
rope_str.iterator(globalThis, &iter);
|
||||
array.ensureStillAlive();
|
||||
|
||||
if (encoder.any_non_ascii) {
|
||||
return .undefined;
|
||||
}
|
||||
|
||||
if (array == .zero) {
|
||||
array = JSC.JSValue.createUninitializedUint8Array(globalThis, length);
|
||||
array.ensureStillAlive();
|
||||
@memcpy(array.asArrayBuffer(globalThis).?.ptr[0..length], buf_to_use[0..length]);
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
pub export fn TextEncoder__encodeInto16(
|
||||
input_ptr: [*]const u16,
|
||||
input_len: usize,
|
||||
buf_ptr: [*]u8,
|
||||
buf_len: usize,
|
||||
) u64 {
|
||||
const output = buf_ptr[0..buf_len];
|
||||
const input = input_ptr[0..input_len];
|
||||
var result: strings.EncodeIntoResult = strings.copyUTF16IntoUTF8(output, []const u16, input, false);
|
||||
if (output.len >= 3 and (result.read == 0 or result.written == 0)) {
|
||||
const replacement_char = [_]u8{ 239, 191, 189 };
|
||||
@memcpy(buf_ptr[0..replacement_char.len], &replacement_char);
|
||||
result.read = 1;
|
||||
result.written = 3;
|
||||
}
|
||||
const sized: [2]u32 = .{ result.read, result.written };
|
||||
return @bitCast(sized);
|
||||
}
|
||||
|
||||
pub export fn TextEncoder__encodeInto8(
|
||||
input_ptr: [*]const u8,
|
||||
input_len: usize,
|
||||
buf_ptr: [*]u8,
|
||||
buf_len: usize,
|
||||
) u64 {
|
||||
const output = buf_ptr[0..buf_len];
|
||||
const input = input_ptr[0..input_len];
|
||||
const result: strings.EncodeIntoResult =
|
||||
strings.copyLatin1IntoUTF8(output, []const u8, input);
|
||||
const sized: [2]u32 = .{ result.read, result.written };
|
||||
return @bitCast(sized);
|
||||
}
|
||||
};
|
||||
|
||||
comptime {
|
||||
_ = TextEncoder.TextEncoder__encode8;
|
||||
_ = TextEncoder.TextEncoder__encode16;
|
||||
_ = TextEncoder.TextEncoder__encodeInto8;
|
||||
_ = TextEncoder.TextEncoder__encodeInto16;
|
||||
_ = TextEncoder.TextEncoder__encodeRopeString;
|
||||
}
|
||||
|
||||
/// https://encoding.spec.whatwg.org/encodings.json
|
||||
pub const EncodingLabel = enum {
|
||||
@"UTF-8",
|
||||
IBM866,
|
||||
@"ISO-8859-2",
|
||||
@"ISO-8859-3",
|
||||
@"ISO-8859-4",
|
||||
@"ISO-8859-5",
|
||||
@"ISO-8859-6",
|
||||
@"ISO-8859-7",
|
||||
@"ISO-8859-8",
|
||||
@"ISO-8859-8-I",
|
||||
@"ISO-8859-10",
|
||||
@"ISO-8859-13",
|
||||
@"ISO-8859-14",
|
||||
@"ISO-8859-15",
|
||||
@"ISO-8859-16",
|
||||
@"KOI8-R",
|
||||
@"KOI8-U",
|
||||
macintosh,
|
||||
@"windows-874",
|
||||
@"windows-1250",
|
||||
@"windows-1251",
|
||||
/// Also known as
|
||||
/// - ASCII
|
||||
/// - latin1
|
||||
@"windows-1252",
|
||||
@"windows-1253",
|
||||
@"windows-1254",
|
||||
@"windows-1255",
|
||||
@"windows-1256",
|
||||
@"windows-1257",
|
||||
@"windows-1258",
|
||||
@"x-mac-cyrillic",
|
||||
Big5,
|
||||
@"EUC-JP",
|
||||
@"ISO-2022-JP",
|
||||
Shift_JIS,
|
||||
@"EUC-KR",
|
||||
@"UTF-16BE",
|
||||
@"UTF-16LE",
|
||||
@"x-user-defined",
|
||||
|
||||
pub const Map = std.enums.EnumMap(EncodingLabel, string);
|
||||
pub const label: Map = brk: {
|
||||
var map = Map.initFull("");
|
||||
map.put(EncodingLabel.@"UTF-8", "utf-8");
|
||||
map.put(EncodingLabel.@"UTF-16LE", "utf-16le");
|
||||
map.put(EncodingLabel.@"windows-1252", "windows-1252");
|
||||
break :brk map;
|
||||
};
|
||||
|
||||
const utf16_names = [_]string{
|
||||
"ucs-2",
|
||||
"utf-16",
|
||||
"unicode",
|
||||
"utf-16le",
|
||||
"csunicode",
|
||||
"unicodefeff",
|
||||
"iso-10646-ucs-2",
|
||||
};
|
||||
|
||||
const utf8_names = [_]string{
|
||||
"utf8",
|
||||
"utf-8",
|
||||
"unicode11utf8",
|
||||
"unicode20utf8",
|
||||
"x-unicode20utf8",
|
||||
"unicode-1-1-utf-8",
|
||||
};
|
||||
|
||||
const latin1_names = [_]string{
|
||||
"l1",
|
||||
"ascii",
|
||||
"cp819",
|
||||
"cp1252",
|
||||
"ibm819",
|
||||
"latin1",
|
||||
"iso88591",
|
||||
"us-ascii",
|
||||
"x-cp1252",
|
||||
"iso8859-1",
|
||||
"iso_8859-1",
|
||||
"iso-8859-1",
|
||||
"iso-ir-100",
|
||||
"csisolatin1",
|
||||
"windows-1252",
|
||||
"ansi_x3.4-1968",
|
||||
"iso_8859-1:1987",
|
||||
};
|
||||
|
||||
pub const latin1 = EncodingLabel.@"windows-1252";
|
||||
|
||||
pub fn which(input_: string) ?EncodingLabel {
|
||||
const input = strings.trim(input_, " \t\r\n");
|
||||
const ExactMatcher = strings.ExactSizeMatcher;
|
||||
const Eight = ExactMatcher(8);
|
||||
const Sixteen = ExactMatcher(16);
|
||||
return switch (input.len) {
|
||||
1, 0 => null,
|
||||
2...8 => switch (Eight.matchLower(input)) {
|
||||
Eight.case("l1"),
|
||||
Eight.case("ascii"),
|
||||
Eight.case("cp819"),
|
||||
Eight.case("cp1252"),
|
||||
Eight.case("ibm819"),
|
||||
Eight.case("latin1"),
|
||||
Eight.case("iso88591"),
|
||||
Eight.case("us-ascii"),
|
||||
Eight.case("x-cp1252"),
|
||||
=> EncodingLabel.latin1,
|
||||
|
||||
Eight.case("ucs-2"),
|
||||
Eight.case("utf-16"),
|
||||
Eight.case("unicode"),
|
||||
Eight.case("utf-16le"),
|
||||
=> EncodingLabel.@"UTF-16LE",
|
||||
|
||||
Eight.case("utf-16be"),
|
||||
=> EncodingLabel.@"UTF-16BE",
|
||||
|
||||
Eight.case("utf8"), Eight.case("utf-8") => EncodingLabel.@"UTF-8",
|
||||
else => null,
|
||||
},
|
||||
|
||||
9...16 => switch (Sixteen.matchLower(input)) {
|
||||
Sixteen.case("iso8859-1"),
|
||||
Sixteen.case("iso_8859-1"),
|
||||
Sixteen.case("iso-8859-1"),
|
||||
Sixteen.case("iso-ir-100"),
|
||||
Sixteen.case("csisolatin1"),
|
||||
Sixteen.case("windows-1252"),
|
||||
Sixteen.case("ansi_x3.4-1968"),
|
||||
Sixteen.case("iso_8859-1:1987"),
|
||||
=> EncodingLabel.latin1,
|
||||
|
||||
Sixteen.case("unicode11utf8"),
|
||||
Sixteen.case("unicode20utf8"),
|
||||
Sixteen.case("x-unicode20utf8"),
|
||||
=> EncodingLabel.@"UTF-8",
|
||||
|
||||
Sixteen.case("csunicode"),
|
||||
Sixteen.case("unicodefeff"),
|
||||
Sixteen.case("iso-10646-ucs-2"),
|
||||
=> EncodingLabel.@"UTF-16LE",
|
||||
|
||||
else => null,
|
||||
},
|
||||
else => if (strings.eqlCaseInsensitiveASCII(input, "unicode-1-1-utf-8", true))
|
||||
EncodingLabel.@"UTF-8"
|
||||
else
|
||||
null,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const TextEncoderStreamEncoder = struct {
|
||||
pending_lead_surrogate: ?u16 = null,
|
||||
|
||||
const log = Output.scoped(.TextEncoderStreamEncoder, false);
|
||||
|
||||
pub usingnamespace JSC.Codegen.JSTextEncoderStreamEncoder;
|
||||
pub usingnamespace bun.New(TextEncoderStreamEncoder);
|
||||
|
||||
pub fn finalize(this: *TextEncoderStreamEncoder) void {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
pub fn constructor(_: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!*TextEncoderStreamEncoder {
|
||||
return TextEncoderStreamEncoder.new(.{});
|
||||
}
|
||||
|
||||
pub fn encode(this: *TextEncoderStreamEncoder, globalObject: *JSC.JSGlobalObject, callFrame: *JSC.CallFrame) bun.JSError!JSValue {
|
||||
const arguments = callFrame.arguments_old(1).slice();
|
||||
if (arguments.len == 0) {
|
||||
return globalObject.throwNotEnoughArguments("TextEncoderStreamEncoder.encode", 1, arguments.len);
|
||||
}
|
||||
|
||||
const str = try arguments[0].getZigString(globalObject);
|
||||
|
||||
if (str.is16Bit()) {
|
||||
return this.encodeUTF16(globalObject, str.utf16SliceAligned());
|
||||
}
|
||||
|
||||
return this.encodeLatin1(globalObject, str.slice());
|
||||
}
|
||||
|
||||
pub fn encodeWithoutTypeChecks(this: *TextEncoderStreamEncoder, globalObject: *JSC.JSGlobalObject, input: *JSC.JSString) JSValue {
|
||||
const str = input.getZigString(globalObject);
|
||||
|
||||
if (str.is16Bit()) {
|
||||
return this.encodeUTF16(globalObject, str.utf16SliceAligned());
|
||||
}
|
||||
|
||||
return this.encodeLatin1(globalObject, str.slice());
|
||||
}
|
||||
|
||||
fn encodeLatin1(this: *TextEncoderStreamEncoder, globalObject: *JSGlobalObject, input: []const u8) JSValue {
|
||||
log("encodeLatin1: \"{s}\"", .{input});
|
||||
|
||||
if (input.len == 0) return JSUint8Array.createEmpty(globalObject);
|
||||
|
||||
const prepend_replacement_len: usize = prepend_replacement: {
|
||||
if (this.pending_lead_surrogate != null) {
|
||||
this.pending_lead_surrogate = null;
|
||||
// no latin1 surrogate pairs
|
||||
break :prepend_replacement 3;
|
||||
}
|
||||
|
||||
break :prepend_replacement 0;
|
||||
};
|
||||
// In a previous benchmark, counting the length took about as much time as allocating the buffer.
|
||||
//
|
||||
// Benchmark Time % CPU (ns) Iterations Ratio
|
||||
// 288.00 ms 13.5% 288.00 ms simdutf::arm64::implementation::convert_latin1_to_utf8(char const*, unsigned long, char*) const
|
||||
// 278.00 ms 13.0% 278.00 ms simdutf::arm64::implementation::utf8_length_from_latin1(char const*, unsigned long) const
|
||||
//
|
||||
//
|
||||
var buffer = std.ArrayList(u8).initCapacity(bun.default_allocator, input.len + prepend_replacement_len) catch {
|
||||
return globalObject.throwOutOfMemoryValue();
|
||||
};
|
||||
if (prepend_replacement_len > 0) {
|
||||
buffer.appendSliceAssumeCapacity(&[3]u8{ 0xef, 0xbf, 0xbd });
|
||||
}
|
||||
|
||||
var remain = input;
|
||||
while (remain.len > 0) {
|
||||
const result = strings.copyLatin1IntoUTF8(buffer.unusedCapacitySlice(), []const u8, remain);
|
||||
|
||||
buffer.items.len += result.written;
|
||||
remain = remain[result.read..];
|
||||
|
||||
if (result.written == 0 and result.read == 0) {
|
||||
buffer.ensureUnusedCapacity(2) catch {
|
||||
buffer.deinit();
|
||||
return globalObject.throwOutOfMemoryValue();
|
||||
};
|
||||
} else if (buffer.items.len == buffer.capacity and remain.len > 0) {
|
||||
buffer.ensureTotalCapacity(buffer.items.len + remain.len + 1) catch {
|
||||
buffer.deinit();
|
||||
return globalObject.throwOutOfMemoryValue();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (comptime Environment.isDebug) {
|
||||
// wrap in comptime if so simdutf isn't called in a release build here.
|
||||
bun.debugAssert(buffer.items.len == (bun.simdutf.length.utf8.from.latin1(input) + prepend_replacement_len));
|
||||
}
|
||||
|
||||
return JSC.JSUint8Array.fromBytes(globalObject, buffer.items);
|
||||
}
|
||||
|
||||
fn encodeUTF16(this: *TextEncoderStreamEncoder, globalObject: *JSGlobalObject, input: []const u16) JSValue {
|
||||
log("encodeUTF16: \"{}\"", .{bun.fmt.utf16(input)});
|
||||
|
||||
if (input.len == 0) return JSUint8Array.createEmpty(globalObject);
|
||||
|
||||
const Prepend = struct {
|
||||
bytes: [4]u8,
|
||||
len: u3,
|
||||
|
||||
pub const replacement: @This() = .{ .bytes = .{ 0xef, 0xbf, 0xbd, 0 }, .len = 3 };
|
||||
|
||||
pub fn fromSequence(seq: [4]u8, length: u3) @This() {
|
||||
return .{ .bytes = seq, .len = length };
|
||||
}
|
||||
};
|
||||
|
||||
var remain = input;
|
||||
|
||||
const prepend: ?Prepend = prepend: {
|
||||
if (this.pending_lead_surrogate) |lead| {
|
||||
this.pending_lead_surrogate = null;
|
||||
const maybe_trail = remain[0];
|
||||
if (strings.u16IsTrail(maybe_trail)) {
|
||||
const converted = strings.utf16CodepointWithFFFD([]const u16, &.{ lead, maybe_trail });
|
||||
// shouldn't fail because `u16IsTrail` is true and `pending_lead_surrogate` is always
|
||||
// a valid lead.
|
||||
bun.debugAssert(!converted.fail);
|
||||
|
||||
const sequence = strings.wtf8Sequence(converted.code_point);
|
||||
|
||||
remain = remain[1..];
|
||||
if (remain.len == 0) {
|
||||
return JSUint8Array.fromBytesCopy(
|
||||
globalObject,
|
||||
sequence[0..converted.utf8Width()],
|
||||
);
|
||||
}
|
||||
|
||||
break :prepend Prepend.fromSequence(sequence, converted.utf8Width());
|
||||
}
|
||||
|
||||
break :prepend Prepend.replacement;
|
||||
}
|
||||
break :prepend null;
|
||||
};
|
||||
|
||||
const length = bun.simdutf.length.utf8.from.utf16.le(remain);
|
||||
|
||||
var buf = std.ArrayList(u8).initCapacity(
|
||||
bun.default_allocator,
|
||||
length + @as(usize, if (prepend) |pre| pre.len else 0),
|
||||
) catch {
|
||||
return globalObject.throwOutOfMemoryValue();
|
||||
};
|
||||
|
||||
if (prepend) |*pre| {
|
||||
buf.appendSliceAssumeCapacity(pre.bytes[0..pre.len]);
|
||||
}
|
||||
|
||||
const result = bun.simdutf.convert.utf16.to.utf8.with_errors.le(remain, buf.unusedCapacitySlice());
|
||||
|
||||
switch (result.status) {
|
||||
else => {
|
||||
// Slow path: there was invalid UTF-16, so we need to convert it without simdutf.
|
||||
const lead_surrogate = strings.toUTF8ListWithTypeBun(&buf, []const u16, remain, true) catch {
|
||||
buf.deinit();
|
||||
return globalObject.throwOutOfMemoryValue();
|
||||
};
|
||||
|
||||
if (lead_surrogate) |pending_lead| {
|
||||
this.pending_lead_surrogate = pending_lead;
|
||||
if (buf.items.len == 0) return JSUint8Array.createEmpty(globalObject);
|
||||
}
|
||||
|
||||
return JSC.JSUint8Array.fromBytes(globalObject, buf.items);
|
||||
},
|
||||
.success => {
|
||||
buf.items.len += result.count;
|
||||
return JSC.JSUint8Array.fromBytes(globalObject, buf.items);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flush(this: *TextEncoderStreamEncoder, globalObject: *JSGlobalObject, _: *JSC.CallFrame) bun.JSError!JSValue {
|
||||
return flushBody(this, globalObject);
|
||||
}
|
||||
|
||||
pub fn flushWithoutTypeChecks(this: *TextEncoderStreamEncoder, globalObject: *JSGlobalObject) JSValue {
|
||||
return flushBody(this, globalObject);
|
||||
}
|
||||
|
||||
fn flushBody(this: *TextEncoderStreamEncoder, globalObject: *JSGlobalObject) JSValue {
|
||||
return if (this.pending_lead_surrogate == null)
|
||||
JSUint8Array.createEmpty(globalObject)
|
||||
else
|
||||
JSUint8Array.fromBytesCopy(globalObject, &.{ 0xef, 0xbf, 0xbd });
|
||||
}
|
||||
};
|
||||
|
||||
pub const TextDecoder = struct {
|
||||
|
||||
// used for utf8 decoding
|
||||
buffered: struct {
|
||||
buf: [3]u8 = .{0} ** 3,
|
||||
len: u2 = 0,
|
||||
|
||||
pub fn slice(this: *@This()) []const u8 {
|
||||
return this.buf[0..this.len];
|
||||
}
|
||||
} = .{},
|
||||
|
||||
// used for utf16 decoding
|
||||
lead_byte: ?u8 = null,
|
||||
lead_surrogate: ?u16 = null,
|
||||
|
||||
ignore_bom: bool = false,
|
||||
fatal: bool = false,
|
||||
encoding: EncodingLabel = EncodingLabel.@"UTF-8",
|
||||
|
||||
pub usingnamespace bun.New(TextDecoder);
|
||||
|
||||
pub fn finalize(this: *TextDecoder) void {
|
||||
this.destroy();
|
||||
}
|
||||
|
||||
pub usingnamespace JSC.Codegen.JSTextDecoder;
|
||||
|
||||
pub fn getIgnoreBOM(
|
||||
this: *TextDecoder,
|
||||
_: *JSC.JSGlobalObject,
|
||||
) JSC.JSValue {
|
||||
return JSC.JSValue.jsBoolean(this.ignore_bom);
|
||||
}
|
||||
|
||||
pub fn getFatal(
|
||||
this: *TextDecoder,
|
||||
_: *JSC.JSGlobalObject,
|
||||
) JSC.JSValue {
|
||||
return JSC.JSValue.jsBoolean(this.fatal);
|
||||
}
|
||||
|
||||
pub fn getEncoding(
|
||||
this: *TextDecoder,
|
||||
globalThis: *JSC.JSGlobalObject,
|
||||
) JSC.JSValue {
|
||||
return ZigString.init(EncodingLabel.label.get(this.encoding).?).toJS(globalThis);
|
||||
}
|
||||
const Vector16 = std.meta.Vector(16, u16);
|
||||
const max_16_ascii: Vector16 = @splat(@as(u16, 127));
|
||||
|
||||
fn processCodeUnitUTF16(
|
||||
this: *TextDecoder,
|
||||
output: *std.ArrayListUnmanaged(u16),
|
||||
saw_error: *bool,
|
||||
code_unit: u16,
|
||||
) error{OutOfMemory}!void {
|
||||
if (this.lead_surrogate) |lead_surrogate| {
|
||||
this.lead_surrogate = null;
|
||||
|
||||
if (strings.u16IsTrail(code_unit)) {
|
||||
// TODO: why is this here?
|
||||
// const code_point = strings.u16GetSupplementary(lead_surrogate, code_unit);
|
||||
try output.appendSlice(
|
||||
bun.default_allocator,
|
||||
&.{ lead_surrogate, code_unit },
|
||||
);
|
||||
return;
|
||||
}
|
||||
try output.append(bun.default_allocator, strings.unicode_replacement);
|
||||
saw_error.* = true;
|
||||
}
|
||||
|
||||
if (strings.u16IsLead(code_unit)) {
|
||||
this.lead_surrogate = code_unit;
|
||||
return;
|
||||
}
|
||||
|
||||
if (strings.u16IsTrail(code_unit)) {
|
||||
try output.append(bun.default_allocator, strings.unicode_replacement);
|
||||
saw_error.* = true;
|
||||
return;
|
||||
}
|
||||
|
||||
try output.append(bun.default_allocator, code_unit);
|
||||
return;
|
||||
}
|
||||
|
||||
pub fn codeUnitFromBytesUTF16(
|
||||
first: u16,
|
||||
second: u16,
|
||||
comptime big_endian: bool,
|
||||
) u16 {
|
||||
return if (comptime big_endian)
|
||||
(first << 8) | second
|
||||
else
|
||||
first | (second << 8);
|
||||
}
|
||||
|
||||
pub fn decodeUTF16(
|
||||
this: *TextDecoder,
|
||||
bytes: []const u8,
|
||||
comptime big_endian: bool,
|
||||
comptime flush: bool,
|
||||
) error{OutOfMemory}!struct { std.ArrayListUnmanaged(u16), bool } {
|
||||
var output: std.ArrayListUnmanaged(u16) = .{};
|
||||
try output.ensureTotalCapacity(bun.default_allocator, @divFloor(bytes.len, 2));
|
||||
|
||||
var remain = bytes;
|
||||
var saw_error = false;
|
||||
|
||||
if (this.lead_byte) |lead_byte| {
|
||||
if (remain.len > 0) {
|
||||
this.lead_byte = null;
|
||||
|
||||
try this.processCodeUnitUTF16(
|
||||
&output,
|
||||
&saw_error,
|
||||
codeUnitFromBytesUTF16(@intCast(lead_byte), @intCast(remain[0]), big_endian),
|
||||
);
|
||||
remain = remain[1..];
|
||||
}
|
||||
}
|
||||
|
||||
var i: usize = 0;
|
||||
|
||||
while (i < remain.len -| 1) {
|
||||
try this.processCodeUnitUTF16(
|
||||
&output,
|
||||
&saw_error,
|
||||
codeUnitFromBytesUTF16(@intCast(remain[i]), @intCast(remain[i + 1]), big_endian),
|
||||
);
|
||||
i += 2;
|
||||
}
|
||||
|
||||
if (remain.len != 0 and i == remain.len - 1) {
|
||||
this.lead_byte = remain[i];
|
||||
} else {
|
||||
bun.assertWithLocation(i == remain.len, @src());
|
||||
}
|
||||
|
||||
if (comptime flush) {
|
||||
if (this.lead_byte != null or this.lead_surrogate != null) {
|
||||
this.lead_byte = null;
|
||||
this.lead_surrogate = null;
|
||||
try output.append(bun.default_allocator, strings.unicode_replacement);
|
||||
saw_error = true;
|
||||
return .{ output, saw_error };
|
||||
}
|
||||
}
|
||||
|
||||
return .{ output, saw_error };
|
||||
}
|
||||
|
||||
pub fn decode(this: *TextDecoder, globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!JSValue {
|
||||
const arguments = callframe.arguments_old(2).slice();
|
||||
|
||||
const input_slice = input_slice: {
|
||||
if (arguments.len == 0 or arguments[0].isUndefined()) {
|
||||
break :input_slice "";
|
||||
}
|
||||
|
||||
if (arguments[0].asArrayBuffer(globalThis)) |array_buffer| {
|
||||
break :input_slice array_buffer.slice();
|
||||
}
|
||||
|
||||
return globalThis.throwInvalidArguments("TextDecoder.decode expects an ArrayBuffer or TypedArray", .{});
|
||||
};
|
||||
|
||||
const stream = stream: {
|
||||
if (arguments.len > 1 and arguments[1].isObject()) {
|
||||
if (arguments[1].fastGet(globalThis, .stream)) |stream_value| {
|
||||
const stream_bool = stream_value.coerce(bool, globalThis);
|
||||
if (globalThis.hasException()) {
|
||||
return .zero;
|
||||
}
|
||||
break :stream stream_bool;
|
||||
}
|
||||
}
|
||||
|
||||
break :stream false;
|
||||
};
|
||||
|
||||
return switch (!stream) {
|
||||
inline else => |flush| this.decodeSlice(globalThis, input_slice, flush),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn decodeWithoutTypeChecks(this: *TextDecoder, globalThis: *JSC.JSGlobalObject, uint8array: *JSC.JSUint8Array) bun.JSError!JSValue {
|
||||
return this.decodeSlice(globalThis, uint8array.slice(), false);
|
||||
}
|
||||
|
||||
fn decodeSlice(this: *TextDecoder, globalThis: *JSC.JSGlobalObject, buffer_slice: []const u8, comptime flush: bool) bun.JSError!JSValue {
|
||||
switch (this.encoding) {
|
||||
EncodingLabel.latin1 => {
|
||||
if (strings.isAllASCII(buffer_slice)) {
|
||||
return ZigString.init(buffer_slice).toJS(globalThis);
|
||||
}
|
||||
|
||||
// It's unintuitive that we encode Latin1 as UTF16 even though the engine natively supports Latin1 strings...
|
||||
// However, this is also what WebKit seems to do.
|
||||
//
|
||||
// It's not clear why we couldn't jusst use Latin1 here, but tests failures proved it necessary.
|
||||
const out_length = strings.elementLengthLatin1IntoUTF16([]const u8, buffer_slice);
|
||||
const bytes = try globalThis.allocator().alloc(u16, out_length);
|
||||
|
||||
const out = strings.copyLatin1IntoUTF16([]u16, bytes, []const u8, buffer_slice);
|
||||
return ZigString.toExternalU16(bytes.ptr, out.written, globalThis);
|
||||
},
|
||||
EncodingLabel.@"UTF-8" => {
|
||||
const input, const deinit = input: {
|
||||
const maybe_without_bom = if (!this.ignore_bom and strings.hasPrefixComptime(buffer_slice, "\xef\xbb\xbf"))
|
||||
buffer_slice[3..]
|
||||
else
|
||||
buffer_slice;
|
||||
|
||||
if (this.buffered.len > 0) {
|
||||
defer this.buffered.len = 0;
|
||||
const joined = try bun.default_allocator.alloc(u8, maybe_without_bom.len + this.buffered.len);
|
||||
@memcpy(joined[0..this.buffered.len], this.buffered.slice());
|
||||
@memcpy(joined[this.buffered.len..][0..maybe_without_bom.len], maybe_without_bom);
|
||||
break :input .{ joined, true };
|
||||
}
|
||||
|
||||
break :input .{ maybe_without_bom, false };
|
||||
};
|
||||
|
||||
const maybe_decode_result = switch (this.fatal) {
|
||||
inline else => |fail_if_invalid| strings.toUTF16AllocMaybeBuffered(bun.default_allocator, input, fail_if_invalid, flush) catch |err| {
|
||||
if (deinit) bun.default_allocator.free(input);
|
||||
if (comptime fail_if_invalid) {
|
||||
if (err == error.InvalidByteSequence) {
|
||||
return globalThis.ERR_ENCODING_INVALID_ENCODED_DATA("Invalid byte sequence", .{}).throw();
|
||||
}
|
||||
}
|
||||
|
||||
bun.assert(err == error.OutOfMemory);
|
||||
return globalThis.throwOutOfMemory();
|
||||
},
|
||||
};
|
||||
|
||||
if (maybe_decode_result) |decode_result| {
|
||||
if (deinit) bun.default_allocator.free(input);
|
||||
const decoded, const leftover, const leftover_len = decode_result;
|
||||
bun.assert(this.buffered.len == 0);
|
||||
if (comptime !flush) {
|
||||
if (leftover_len != 0) {
|
||||
this.buffered.buf = leftover;
|
||||
this.buffered.len = leftover_len;
|
||||
}
|
||||
}
|
||||
return ZigString.toExternalU16(decoded.ptr, decoded.len, globalThis);
|
||||
}
|
||||
|
||||
bun.debugAssert(input.len == 0 or !deinit);
|
||||
|
||||
// Experiment: using mimalloc directly is slightly slower
|
||||
return ZigString.init(input).toJS(globalThis);
|
||||
},
|
||||
|
||||
inline .@"UTF-16LE", .@"UTF-16BE" => |utf16_encoding| {
|
||||
const bom = if (comptime utf16_encoding == .@"UTF-16LE") "\xff\xfe" else "\xfe\xff";
|
||||
const input = if (!this.ignore_bom and strings.hasPrefixComptime(buffer_slice, bom))
|
||||
buffer_slice[2..]
|
||||
else
|
||||
buffer_slice;
|
||||
|
||||
var decoded, const saw_error = try this.decodeUTF16(input, utf16_encoding == .@"UTF-16BE", flush);
|
||||
|
||||
if (saw_error and this.fatal) {
|
||||
decoded.deinit(bun.default_allocator);
|
||||
return globalThis.ERR_ENCODING_INVALID_ENCODED_DATA("The encoded data was not valid {s} data", .{@tagName(utf16_encoding)}).throw();
|
||||
}
|
||||
|
||||
var output = bun.String.fromUTF16(decoded.items);
|
||||
return output.toJS(globalThis);
|
||||
},
|
||||
else => {
|
||||
return globalThis.throwInvalidArguments("TextDecoder.decode set to unsupported encoding", .{});
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn constructor(globalThis: *JSC.JSGlobalObject, callframe: *JSC.CallFrame) bun.JSError!*TextDecoder {
|
||||
var args_ = callframe.arguments_old(2);
|
||||
var arguments: []const JSC.JSValue = args_.ptr[0..args_.len];
|
||||
|
||||
var decoder = TextDecoder{};
|
||||
|
||||
if (arguments.len > 0) {
|
||||
// encoding
|
||||
if (arguments[0].isString()) {
|
||||
var str = try arguments[0].toSlice(globalThis, bun.default_allocator);
|
||||
defer if (str.isAllocated()) str.deinit();
|
||||
|
||||
if (EncodingLabel.which(str.slice())) |label| {
|
||||
decoder.encoding = label;
|
||||
} else {
|
||||
return globalThis.throwInvalidArguments("Unsupported encoding label \"{s}\"", .{str.slice()});
|
||||
}
|
||||
} else if (arguments[0].isUndefined()) {
|
||||
// default to utf-8
|
||||
decoder.encoding = EncodingLabel.@"UTF-8";
|
||||
} else {
|
||||
return globalThis.throwInvalidArguments("TextDecoder(encoding) label is invalid", .{});
|
||||
}
|
||||
|
||||
if (arguments.len >= 2) {
|
||||
const options = arguments[1];
|
||||
|
||||
if (!options.isObject()) {
|
||||
return globalThis.throwInvalidArguments("TextDecoder(options) is invalid", .{});
|
||||
}
|
||||
|
||||
if (try options.get(globalThis, "fatal")) |fatal| {
|
||||
if (fatal.isBoolean()) {
|
||||
decoder.fatal = fatal.asBoolean();
|
||||
} else {
|
||||
return globalThis.throwInvalidArguments("TextDecoder(options) fatal is invalid. Expected boolean value", .{});
|
||||
}
|
||||
}
|
||||
|
||||
if (try options.get(globalThis, "ignoreBOM")) |ignoreBOM| {
|
||||
if (ignoreBOM.isBoolean()) {
|
||||
decoder.ignore_bom = ignoreBOM.asBoolean();
|
||||
} else {
|
||||
return globalThis.throwInvalidArguments("TextDecoder(options) ignoreBOM is invalid. Expected boolean value", .{});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return TextDecoder.new(decoder);
|
||||
}
|
||||
};
|
||||
pub const TextEncoder = @import("./TextEncoder.zig");
|
||||
pub const EncodingLabel = @import("./EncodingLabel.zig").EncodingLabel;
|
||||
pub const TextEncoderStreamEncoder = @import("./TextEncoderStreamEncoder.zig");
|
||||
pub const TextDecoder = @import("./TextDecoder.zig");
|
||||
|
||||
pub const Encoder = struct {
|
||||
export fn Bun__encoding__writeLatin1(input: [*]const u8, len: usize, to: [*]u8, to_len: usize, encoding: u8) usize {
|
||||
@@ -1468,22 +529,23 @@ pub const Encoder = struct {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
comptime {
|
||||
_ = Bun__encoding__writeLatin1;
|
||||
_ = Bun__encoding__writeUTF16;
|
||||
|
||||
_ = Bun__encoding__byteLengthLatin1AsUTF8;
|
||||
_ = Bun__encoding__byteLengthUTF16AsUTF8;
|
||||
|
||||
_ = Bun__encoding__toString;
|
||||
_ = Bun__encoding__toStringUTF8;
|
||||
|
||||
_ = Bun__encoding__constructFromLatin1;
|
||||
_ = Bun__encoding__constructFromUTF16;
|
||||
}
|
||||
};
|
||||
|
||||
comptime {
|
||||
std.testing.refAllDecls(Encoder);
|
||||
_ = &TextEncoder.TextEncoder__encode8;
|
||||
_ = &TextEncoder.TextEncoder__encode16;
|
||||
_ = &TextEncoder.TextEncoder__encodeInto8;
|
||||
_ = &TextEncoder.TextEncoder__encodeInto16;
|
||||
_ = &TextEncoder.TextEncoder__encodeRopeString;
|
||||
}
|
||||
|
||||
comptime {
|
||||
_ = &Encoder.Bun__encoding__writeLatin1;
|
||||
_ = &Encoder.Bun__encoding__writeUTF16;
|
||||
_ = &Encoder.Bun__encoding__byteLengthLatin1AsUTF8;
|
||||
_ = &Encoder.Bun__encoding__byteLengthUTF16AsUTF8;
|
||||
_ = &Encoder.Bun__encoding__toString;
|
||||
_ = &Encoder.Bun__encoding__toStringUTF8;
|
||||
_ = &Encoder.Bun__encoding__constructFromLatin1;
|
||||
_ = &Encoder.Bun__encoding__constructFromUTF16;
|
||||
}
|
||||
|
||||
@@ -588,11 +588,11 @@ pub const RunCommand = struct {
|
||||
const is_probably_trying_to_run_a_pkg_script =
|
||||
original_script_for_bun_run != null and
|
||||
((code == 1 and bun.strings.eqlComptime(original_script_for_bun_run.?, "test")) or
|
||||
(code == 2 and bun.strings.eqlAnyComptime(original_script_for_bun_run.?, &.{
|
||||
"install",
|
||||
"kill",
|
||||
"link",
|
||||
}) and ctx.positionals.len == 1));
|
||||
(code == 2 and bun.strings.eqlAnyComptime(original_script_for_bun_run.?, &.{
|
||||
"install",
|
||||
"kill",
|
||||
"link",
|
||||
}) and ctx.positionals.len == 1));
|
||||
|
||||
if (is_probably_trying_to_run_a_pkg_script) {
|
||||
// if you run something like `bun run test`, you get a confusing message because
|
||||
@@ -1077,9 +1077,7 @@ pub const RunCommand = struct {
|
||||
bun.copy(u8, path_buf[dir_slice.len..], base);
|
||||
path_buf[dir_slice.len + base.len] = 0;
|
||||
const slice = path_buf[0 .. dir_slice.len + base.len :0];
|
||||
if (Environment.isWindows) {
|
||||
@panic("TODO");
|
||||
}
|
||||
|
||||
if (!(bun.sys.isExecutableFilePath(slice))) continue;
|
||||
// we need to dupe because the string pay point to a pointer that only exists in the current scope
|
||||
_ = try results.getOrPut(this_transpiler.fs.filename_store.append(@TypeOf(base), base) catch continue);
|
||||
|
||||
@@ -82,7 +82,7 @@ pub fn isParentOrEqual(parent_: []const u8, child: []const u8) ParentEqual {
|
||||
if (!contains(child, parent)) return .unrelated;
|
||||
|
||||
if (child.len == parent.len) return .equal;
|
||||
if (isSepAny(child[parent.len])) return .parent;
|
||||
if (child.len > parent.len and isSepAny(child[parent.len])) return .parent;
|
||||
return .unrelated;
|
||||
}
|
||||
|
||||
|
||||
@@ -78,3 +78,17 @@ describe("banned words", () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("files that must have comments at the top", () => {
|
||||
const files = ["src/bun.js/api/BunObject.zig"];
|
||||
|
||||
for (const file of files) {
|
||||
test(file, async () => {
|
||||
const joined = path.join(import.meta.dir, "..", "..", file);
|
||||
const content = await Bun.file(joined).text();
|
||||
if (!content.startsWith("//")) {
|
||||
throw new Error(`Please don't add imports to the top of ${file}. Put them at the bottom.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -541,3 +541,12 @@ it("throws a validation error when routes object is undefined and fetch is not s
|
||||
Learn more at https://bun.sh/docs/api/http"
|
||||
`);
|
||||
});
|
||||
|
||||
it("don't crash on server.fetch()", async () => {
|
||||
await using server = Bun.serve({
|
||||
port: 0,
|
||||
routes: { "/test": () => new Response("test") },
|
||||
});
|
||||
|
||||
expect(server.fetch("/test")).rejects.toThrow("fetch() requires the server to have a fetch handler");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user