Merge pull request #222 from lightpanda-io/webstorage

storage: first implementation of webstorage API
This commit is contained in:
Pierre Tachoire
2024-05-02 15:35:22 +02:00
committed by GitHub
8 changed files with 291 additions and 1 deletions

View File

@@ -6,6 +6,7 @@ const DOM = @import("dom/dom.zig");
const HTML = @import("html/html.zig"); const HTML = @import("html/html.zig");
const Events = @import("events/event.zig"); const Events = @import("events/event.zig");
const XHR = @import("xhr/xhr.zig"); const XHR = @import("xhr/xhr.zig");
const Storage = @import("storage/storage.zig");
pub const HTMLDocument = @import("html/document.zig").HTMLDocument; pub const HTMLDocument = @import("html/document.zig").HTMLDocument;
@@ -16,4 +17,5 @@ pub const Interfaces = generate.Tuple(.{
Events.Interfaces, Events.Interfaces,
HTML.Interfaces, HTML.Interfaces,
XHR.Interfaces, XHR.Interfaces,
Storage.Interfaces,
}); });

View File

@@ -17,6 +17,8 @@ const apiweb = @import("../apiweb.zig");
const Window = @import("../html/window.zig").Window; const Window = @import("../html/window.zig").Window;
const Walker = @import("../dom/walker.zig").WalkerDepthFirst; const Walker = @import("../dom/walker.zig").WalkerDepthFirst;
const storage = @import("../storage/storage.zig");
const FetchResult = std.http.Client.FetchResult; const FetchResult = std.http.Client.FetchResult;
const log = std.log.scoped(.browser); const log = std.log.scoped(.browser);
@@ -69,6 +71,8 @@ pub const Session = struct {
env: Env = undefined, env: Env = undefined,
loop: Loop, loop: Loop,
window: Window, window: Window,
// TODO move the shed to the browser?
storageShed: storage.Shed,
jstypes: [Types.len]usize = undefined, jstypes: [Types.len]usize = undefined,
@@ -81,6 +85,7 @@ pub const Session = struct {
.window = Window.create(null), .window = Window.create(null),
.loader = Loader.init(alloc), .loader = Loader.init(alloc),
.loop = try Loop.init(alloc), .loop = try Loop.init(alloc),
.storageShed = storage.Shed.init(alloc),
}; };
self.env = try Env.init(self.arena.allocator(), &self.loop); self.env = try Env.init(self.arena.allocator(), &self.loop);
@@ -95,6 +100,7 @@ pub const Session = struct {
self.loader.deinit(); self.loader.deinit();
self.loop.deinit(); self.loop.deinit();
self.storageShed.deinit();
self.alloc.destroy(self); self.alloc.destroy(self);
} }
@@ -116,6 +122,7 @@ pub const Page = struct {
// handle url // handle url
rawuri: ?[]const u8 = null, rawuri: ?[]const u8 = null,
uri: std.Uri = undefined, uri: std.Uri = undefined,
origin: ?[]const u8 = null,
raw_data: ?[]const u8 = null, raw_data: ?[]const u8 = null,
@@ -169,6 +176,15 @@ pub const Page = struct {
self.rawuri = try alloc.dupe(u8, uri); self.rawuri = try alloc.dupe(u8, uri);
self.uri = std.Uri.parse(self.rawuri.?) catch try std.Uri.parseWithoutScheme(self.rawuri.?); self.uri = std.Uri.parse(self.rawuri.?) catch try std.Uri.parseWithoutScheme(self.rawuri.?);
// prepare origin value.
var buf = std.ArrayList(u8).init(alloc);
defer buf.deinit();
try self.uri.writeToStream(.{
.scheme = true,
.authority = true,
}, buf.writer());
self.origin = try buf.toOwnedSlice();
// TODO handle fragment in url. // TODO handle fragment in url.
// load the data // load the data
@@ -237,6 +253,9 @@ pub const Page = struct {
// TODO set the referrer to the document. // TODO set the referrer to the document.
self.session.window.replaceDocument(html_doc); self.session.window.replaceDocument(html_doc);
self.session.window.setStorageShelf(
try self.session.storageShed.getOrPut(self.origin orelse "null"),
);
// https://html.spec.whatwg.org/#read-html // https://html.spec.whatwg.org/#read-html

View File

@@ -4,6 +4,8 @@ const parser = @import("../netsurf.zig");
const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventTarget = @import("../dom/event_target.zig").EventTarget;
const storage = @import("../storage/storage.zig");
// https://dom.spec.whatwg.org/#interface-window-extensions // https://dom.spec.whatwg.org/#interface-window-extensions
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window // https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
pub const Window = struct { pub const Window = struct {
@@ -17,6 +19,8 @@ pub const Window = struct {
document: ?*parser.DocumentHTML = null, document: ?*parser.DocumentHTML = null,
target: []const u8, target: []const u8,
storageShelf: ?*storage.Shelf = null,
pub fn create(target: ?[]const u8) Window { pub fn create(target: ?[]const u8) Window {
return Window{ return Window{
.target = target orelse "", .target = target orelse "",
@@ -27,6 +31,10 @@ pub const Window = struct {
self.document = doc; self.document = doc;
} }
pub fn setStorageShelf(self: *Window, shelf: *storage.Shelf) void {
self.storageShelf = shelf;
}
pub fn get_window(self: *Window) *Window { pub fn get_window(self: *Window) *Window {
return self; return self;
} }
@@ -46,4 +54,14 @@ pub const Window = struct {
pub fn get_name(self: *Window) []const u8 { pub fn get_name(self: *Window) []const u8 {
return self.target; return self.target;
} }
pub fn get_localStorage(self: *Window) !*storage.Bottle {
if (self.storageShelf == null) return parser.DOMError.NotSupported;
return &self.storageShelf.?.bucket.local;
}
pub fn get_sessionStorage(self: *Window) !*storage.Bottle {
if (self.storageShelf == null) return parser.DOMError.NotSupported;
return &self.storageShelf.?.bucket.session;
}
}; };

View File

@@ -5,6 +5,7 @@ const jsruntime = @import("jsruntime");
const parser = @import("netsurf.zig"); const parser = @import("netsurf.zig");
const apiweb = @import("apiweb.zig"); const apiweb = @import("apiweb.zig");
const Window = @import("html/window.zig").Window; const Window = @import("html/window.zig").Window;
const storage = @import("storage/storage.zig");
const html_test = @import("html_test.zig").html; const html_test = @import("html_test.zig").html;
@@ -20,9 +21,13 @@ fn execJS(
try js_env.start(alloc); try js_env.start(alloc);
defer js_env.stop(); defer js_env.stop();
var storageShelf = storage.Shelf.init(alloc);
defer storageShelf.deinit();
// alias global as self and window // alias global as self and window
var window = Window.create(null); var window = Window.create(null);
window.replaceDocument(doc); window.replaceDocument(doc);
window.setStorageShelf(&storageShelf);
try js_env.bindGlobal(window); try js_env.bindGlobal(window);
// launch shellExec // launch shellExec

View File

@@ -9,6 +9,7 @@ const parser = @import("netsurf.zig");
const apiweb = @import("apiweb.zig"); const apiweb = @import("apiweb.zig");
const Window = @import("html/window.zig").Window; const Window = @import("html/window.zig").Window;
const xhr = @import("xhr/xhr.zig"); const xhr = @import("xhr/xhr.zig");
const storage = @import("storage/storage.zig");
const documentTestExecFn = @import("dom/document.zig").testExecFn; const documentTestExecFn = @import("dom/document.zig").testExecFn;
const HTMLDocumentTestExecFn = @import("html/document.zig").testExecFn; const HTMLDocumentTestExecFn = @import("html/document.zig").testExecFn;
@@ -28,6 +29,7 @@ const ProcessingInstructionTestExecFn = @import("dom/processing_instruction.zig"
const EventTestExecFn = @import("events/event.zig").testExecFn; const EventTestExecFn = @import("events/event.zig").testExecFn;
const XHRTestExecFn = xhr.testExecFn; const XHRTestExecFn = xhr.testExecFn;
const ProgressEventTestExecFn = @import("xhr/progress_event.zig").testExecFn; const ProgressEventTestExecFn = @import("xhr/progress_event.zig").testExecFn;
const StorageTestExecFn = storage.testExecFn;
pub const Types = jsruntime.reflect(apiweb.Interfaces); pub const Types = jsruntime.reflect(apiweb.Interfaces);
@@ -45,6 +47,9 @@ fn testExecFn(
try js_env.start(alloc); try js_env.start(alloc);
defer js_env.stop(); defer js_env.stop();
var storageShelf = storage.Shelf.init(alloc);
defer storageShelf.deinit();
// document // document
const file = try std.fs.cwd().openFile("test.html", .{}); const file = try std.fs.cwd().openFile("test.html", .{});
defer file.close(); defer file.close();
@@ -56,7 +61,10 @@ fn testExecFn(
// alias global as self and window // alias global as self and window
var window = Window.create(null); var window = Window.create(null);
window.replaceDocument(doc); window.replaceDocument(doc);
window.setStorageShelf(&storageShelf);
try js_env.bindGlobal(window); try js_env.bindGlobal(window);
// run test // run test
@@ -86,6 +94,7 @@ fn testsAllExecFn(
XHRTestExecFn, XHRTestExecFn,
ProgressEventTestExecFn, ProgressEventTestExecFn,
ProcessingInstructionTestExecFn, ProcessingInstructionTestExecFn,
StorageTestExecFn,
}; };
inline for (testFns) |testFn| { inline for (testFns) |testFn| {

232
src/storage/storage.zig Normal file
View File

@@ -0,0 +1,232 @@
const std = @import("std");
const jsruntime = @import("jsruntime");
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const generate = @import("../generate.zig");
const DOMError = @import("../netsurf.zig").DOMError;
const log = std.log.scoped(.storage);
pub const Interfaces = generate.Tuple(.{
Bottle,
});
// See https://storage.spec.whatwg.org/#model for storage hierarchy.
// A Shed contains map of Shelves. The key is the document's origin.
// A Shelf contains on default Bucket (it could contain many in the future).
// A Bucket contains a local and a session Bottle.
// A Bottle stores a map of strings and is exposed to the JS.
pub const Shed = struct {
const Map = std.StringHashMapUnmanaged(Shelf);
alloc: std.mem.Allocator,
map: Map,
pub fn init(alloc: std.mem.Allocator) Shed {
return .{
.alloc = alloc,
.map = .{},
};
}
pub fn deinit(self: *Shed) void {
// loop hover each KV and free the memory.
var it = self.map.iterator();
while (it.next()) |entry| {
entry.value_ptr.deinit();
self.alloc.free(entry.key_ptr.*);
}
self.map.deinit(self.alloc);
}
pub fn getOrPut(self: *Shed, origin: []const u8) !*Shelf {
const shelf = self.map.getPtr(origin);
if (shelf) |s| return s;
const oorigin = try self.alloc.dupe(u8, origin);
try self.map.put(self.alloc, oorigin, Shelf.init(self.alloc));
return self.map.getPtr(origin).?;
}
};
pub const Shelf = struct {
bucket: Bucket,
pub fn init(alloc: std.mem.Allocator) Shelf {
return .{ .bucket = Bucket.init(alloc) };
}
pub fn deinit(self: *Shelf) void {
self.bucket.deinit();
}
};
pub const Bucket = struct {
local: Bottle,
session: Bottle,
pub fn init(alloc: std.mem.Allocator) Bucket {
return .{
.local = Bottle.init(alloc),
.session = Bottle.init(alloc),
};
}
pub fn deinit(self: *Bucket) void {
self.local.deinit();
self.session.deinit();
}
};
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storage-interface
pub const Bottle = struct {
pub const mem_guarantied = true;
const Map = std.StringHashMapUnmanaged([]const u8);
// allocator is stored. we don't use the JS env allocator b/c the storage
// data could exists longer than a js env lifetime.
alloc: std.mem.Allocator,
map: Map,
pub fn init(alloc: std.mem.Allocator) Bottle {
return .{
.alloc = alloc,
.map = .{},
};
}
// loop hover each KV and free the memory.
fn free(self: *Bottle) void {
var it = self.map.iterator();
while (it.next()) |entry| {
self.alloc.free(entry.key_ptr.*);
self.alloc.free(entry.value_ptr.*);
}
}
pub fn deinit(self: *Bottle) void {
self.free();
self.map.deinit(self.alloc);
}
pub fn get_length(self: *Bottle) u32 {
return @intCast(self.map.count());
}
pub fn _key(self: *Bottle, idx: u32) ?[]const u8 {
if (idx >= self.map.count()) return null;
var it = self.map.valueIterator();
var i: u32 = 0;
while (it.next()) |v| {
if (i == idx) return v.*;
i += 1;
}
unreachable;
}
pub fn _getItem(self: *Bottle, k: []const u8) ?[]const u8 {
return self.map.get(k);
}
pub fn _setItem(self: *Bottle, k: []const u8, v: []const u8) !void {
const old = self.map.get(k);
if (old != null and std.mem.eql(u8, v, old.?)) return;
// owns k and v by copying them.
const kk = try self.alloc.dupe(u8, k);
errdefer self.alloc.free(kk);
const vv = try self.alloc.dupe(u8, v);
errdefer self.alloc.free(vv);
self.map.put(self.alloc, kk, vv) catch |e| {
log.debug("set item: {any}", .{e});
return DOMError.QuotaExceeded;
};
// > Broadcast this with key, oldValue, and value.
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storageevent-interface
//
// > The storage event of the Window interface fires when a storage
// > area (localStorage or sessionStorage) has been modified in the
// > context of another document.
// https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event
//
// So for now, we won't impement the feature.
}
pub fn _removeItem(self: *Bottle, k: []const u8) !void {
const old = self.map.fetchRemove(k);
if (old == null) return;
// > Broadcast this with key, oldValue, and null.
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storageevent-interface
//
// > The storage event of the Window interface fires when a storage
// > area (localStorage or sessionStorage) has been modified in the
// > context of another document.
// https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event
//
// So for now, we won't impement the feature.
}
pub fn _clear(self: *Bottle) void {
self.free();
self.map.clearRetainingCapacity();
// > Broadcast this with null, null, and null.
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storageevent-interface
//
// > The storage event of the Window interface fires when a storage
// > area (localStorage or sessionStorage) has been modified in the
// > context of another document.
// https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event
//
// So for now, we won't impement the feature.
}
};
// Tests
// -----
pub fn testExecFn(
_: std.mem.Allocator,
js_env: *jsruntime.Env,
) anyerror!void {
var storage = [_]Case{
.{ .src = "localStorage.length", .ex = "0" },
.{ .src = "localStorage.setItem('foo', 'bar')", .ex = "undefined" },
.{ .src = "localStorage.length", .ex = "1" },
.{ .src = "localStorage.getItem('foo')", .ex = "bar" },
.{ .src = "localStorage.removeItem('foo')", .ex = "undefined" },
.{ .src = "localStorage.length", .ex = "0" },
// .{ .src = "localStorage['foo'] = 'bar'", .ex = "undefined" },
// .{ .src = "localStorage['foo']", .ex = "bar" },
// .{ .src = "localStorage.length", .ex = "1" },
.{ .src = "localStorage.clear()", .ex = "undefined" },
.{ .src = "localStorage.length", .ex = "0" },
};
try checkCases(js_env, &storage);
}
test "storage bottle" {
var bottle = Bottle.init(std.testing.allocator);
defer bottle.deinit();
try std.testing.expect(0 == bottle.get_length());
try std.testing.expect(null == bottle._getItem("foo"));
try bottle._setItem("foo", "bar");
try std.testing.expect(std.mem.eql(u8, "bar", bottle._getItem("foo").?));
try bottle._removeItem("foo");
try std.testing.expect(0 == bottle.get_length());
try std.testing.expect(null == bottle._getItem("foo"));
}

View File

@@ -9,6 +9,7 @@ const jsruntime = @import("jsruntime");
const Loop = jsruntime.Loop; const Loop = jsruntime.Loop;
const Env = jsruntime.Env; const Env = jsruntime.Env;
const Window = @import("../html/window.zig").Window; const Window = @import("../html/window.zig").Window;
const storage = @import("../storage/storage.zig");
const Types = @import("../main_wpt.zig").Types; const Types = @import("../main_wpt.zig").Types;
@@ -34,6 +35,9 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
var js_env = try Env.init(alloc, &loop); var js_env = try Env.init(alloc, &loop);
defer js_env.deinit(); defer js_env.deinit();
var storageShelf = storage.Shelf.init(alloc);
defer storageShelf.deinit();
// load user-defined types in JS env // load user-defined types in JS env
var js_types: [Types.len]usize = undefined; var js_types: [Types.len]usize = undefined;
try js_env.load(&js_types); try js_env.load(&js_types);
@@ -54,6 +58,7 @@ pub fn run(arena: *std.heap.ArenaAllocator, comptime dir: []const u8, f: []const
// setup global env vars. // setup global env vars.
var window = Window.create(null); var window = Window.create(null);
window.replaceDocument(html_doc); window.replaceDocument(html_doc);
window.setStorageShelf(&storageShelf);
try js_env.bindGlobal(window); try js_env.bindGlobal(window);
// thanks to the arena, we don't need to deinit res. // thanks to the arena, we don't need to deinit res.