pub const S3ListObjectsOptions = struct { continuation_token: ?[]const u8, delimiter: ?[]const u8, encoding_type: ?[]const u8, fetch_owner: ?bool, max_keys: ?i64, prefix: ?[]const u8, start_after: ?[]const u8, _continuation_token: ?jsc.ZigString.Slice, _delimiter: ?jsc.ZigString.Slice, _encoding_type: ?jsc.ZigString.Slice, _prefix: ?jsc.ZigString.Slice, _start_after: ?jsc.ZigString.Slice, pub fn deinit(this: *@This()) void { if (this._continuation_token) |slice| slice.deinit(); if (this._delimiter) |slice| slice.deinit(); if (this._encoding_type) |slice| slice.deinit(); if (this._prefix) |slice| slice.deinit(); if (this._start_after) |slice| slice.deinit(); } }; const ObjectOwner = struct { id: ?[]const u8, display_name: ?[]const u8, }; const ObjectRestoreStatus = struct { is_restore_in_progress: ?bool, restore_expiry_date: ?[]const u8, }; const S3ListObjectsContents = struct { key: []const u8, etag: ?[]const u8, checksum_type: ?[]const u8, checksum_algorithme: ?[]const u8, last_modified: ?[]const u8, object_size: ?i64, storage_class: ?[]const u8, owner: ?ObjectOwner, restore_status: ?ObjectRestoreStatus, }; pub const S3ListObjectsV2Result = struct { name: ?[]const u8, prefix: ?[]const u8, key_count: ?i64, max_keys: ?i64, delimiter: ?[]const u8, encoding_type: ?[]const u8, is_truncated: ?bool, continuation_token: ?[]const u8, next_continuation_token: ?[]const u8, start_after: ?[]const u8, common_prefixes: ?std.ArrayList([]const u8), contents: ?std.ArrayList(S3ListObjectsContents), pub fn deinit(this: @This()) void { if (this.contents) |contents| { contents.deinit(); } if (this.common_prefixes) |common_prefixes| { common_prefixes.deinit(); } } pub fn toJS(this: @This(), globalObject: *JSGlobalObject) bun.JSError!JSValue { const jsResult = JSValue.createEmptyObject(globalObject, 12); if (this.name) |name| { jsResult.put(globalObject, jsc.ZigString.static("name"), try bun.String.createUTF8ForJS(globalObject, name)); } if (this.prefix) |prefix| { jsResult.put(globalObject, jsc.ZigString.static("prefix"), try bun.String.createUTF8ForJS(globalObject, prefix)); } if (this.delimiter) |delimiter| { jsResult.put(globalObject, jsc.ZigString.static("delimiter"), try bun.String.createUTF8ForJS(globalObject, delimiter)); } if (this.start_after) |start_after| { jsResult.put(globalObject, jsc.ZigString.static("startAfter"), try bun.String.createUTF8ForJS(globalObject, start_after)); } if (this.encoding_type) |encoding_type| { jsResult.put(globalObject, jsc.ZigString.static("encodingType"), try bun.String.createUTF8ForJS(globalObject, encoding_type)); } if (this.continuation_token) |continuation_token| { jsResult.put(globalObject, jsc.ZigString.static("continuationToken"), try bun.String.createUTF8ForJS(globalObject, continuation_token)); } if (this.next_continuation_token) |next_continuation_token| { jsResult.put(globalObject, jsc.ZigString.static("nextContinuationToken"), try bun.String.createUTF8ForJS(globalObject, next_continuation_token)); } if (this.is_truncated) |is_truncated| { jsResult.put(globalObject, jsc.ZigString.static("isTruncated"), JSValue.jsBoolean(is_truncated)); } if (this.key_count) |key_count| { jsResult.put(globalObject, jsc.ZigString.static("keyCount"), JSValue.jsNumber(key_count)); } if (this.max_keys) |max_keys| { jsResult.put(globalObject, jsc.ZigString.static("maxKeys"), JSValue.jsNumber(max_keys)); } if (this.contents) |contents| { const jsContents = try JSValue.createEmptyArray(globalObject, contents.items.len); for (contents.items, 0..) |item, i| { const objectInfo = JSValue.createEmptyObject(globalObject, 1); objectInfo.put(globalObject, jsc.ZigString.static("key"), try bun.String.createUTF8ForJS(globalObject, item.key)); if (item.etag) |etag| { objectInfo.put(globalObject, jsc.ZigString.static("eTag"), try bun.String.createUTF8ForJS(globalObject, etag)); } if (item.checksum_algorithme) |checksum_algorithme| { objectInfo.put(globalObject, jsc.ZigString.static("checksumAlgorithme"), try bun.String.createUTF8ForJS(globalObject, checksum_algorithme)); } if (item.checksum_type) |checksum_type| { objectInfo.put(globalObject, jsc.ZigString.static("checksumType"), try bun.String.createUTF8ForJS(globalObject, checksum_type)); } if (item.last_modified) |last_modified| { objectInfo.put(globalObject, jsc.ZigString.static("lastModified"), try bun.String.createUTF8ForJS(globalObject, last_modified)); } if (item.object_size) |object_size| { objectInfo.put(globalObject, jsc.ZigString.static("size"), JSValue.jsNumber(object_size)); } if (item.storage_class) |storage_class| { objectInfo.put(globalObject, jsc.ZigString.static("storageClass"), try bun.String.createUTF8ForJS(globalObject, storage_class)); } if (item.owner) |owner| { const jsOwner = JSValue.createEmptyObject(globalObject, 2); if (owner.id) |id| { jsOwner.put(globalObject, jsc.ZigString.static("id"), try bun.String.createUTF8ForJS(globalObject, id)); } if (owner.display_name) |display_name| { jsOwner.put(globalObject, jsc.ZigString.static("displayName"), try bun.String.createUTF8ForJS(globalObject, display_name)); } objectInfo.put(globalObject, jsc.ZigString.static("owner"), jsOwner); } try jsContents.putIndex(globalObject, @intCast(i), objectInfo); } jsResult.put(globalObject, jsc.ZigString.static("contents"), jsContents); } if (this.common_prefixes) |common_prefixes| { const jsCommonPrefixes = try JSValue.createEmptyArray(globalObject, common_prefixes.items.len); for (common_prefixes.items, 0..) |prefix, i| { const jsPrefix = JSValue.createEmptyObject(globalObject, 1); jsPrefix.put(globalObject, jsc.ZigString.static("prefix"), try bun.String.createUTF8ForJS(globalObject, prefix)); try jsCommonPrefixes.putIndex(globalObject, @intCast(i), jsPrefix); } jsResult.put(globalObject, jsc.ZigString.static("commonPrefixes"), jsCommonPrefixes); } return jsResult; } }; pub fn parseS3ListObjectsResult(xml: []const u8) !S3ListObjectsV2Result { var result: S3ListObjectsV2Result = .{ .contents = null, .common_prefixes = null, .continuation_token = null, .delimiter = null, .encoding_type = null, .is_truncated = null, .key_count = null, .max_keys = null, .name = null, .next_continuation_token = null, .prefix = null, .start_after = null, }; var contents = std.ArrayList(S3ListObjectsContents).init(bun.default_allocator); var common_prefixes = std.ArrayList([]const u8).init(bun.default_allocator); // we dont use trailing ">" as it may finish with xmlns=... if (strings.indexOf(xml, "")) |end| { i = i + 1; const tag_name_end_pos = i + end; // +1 for < const tagName = xml[i..tag_name_end_pos]; i = tag_name_end_pos + 1; // +1 for > if (strings.eql(tagName, "Contents")) { var looking_for_end_tag = true; var object_key: ?[]const u8 = null; var last_modified: ?[]const u8 = null; var object_size: ?i64 = null; var storage_class: ?[]const u8 = null; var etag: ?[]const u8 = null; var checksum_type: ?[]const u8 = null; var checksum_algorithme: ?[]const u8 = null; var owner_id: ?[]const u8 = null; var owner_display_name: ?[]const u8 = null; var is_restore_in_progress: ?bool = null; var restore_expiry_date: ?[]const u8 = null; while (looking_for_end_tag) { if (i >= xml.len) { break; } if (xml[i] == '<') { if (strings.indexOf(xml[i + 1 ..], ">")) |__end| { const inner_tag_name_or_tag_end = xml[i + 1 .. i + 1 + __end]; i = i + 2 + __end; if (strings.eql(inner_tag_name_or_tag_end, "/Contents")) { looking_for_end_tag = false; } else if (strings.eql(inner_tag_name_or_tag_end, "Key")) { if (strings.indexOf(xml[i..], "")) |__tag_end| { object_key = xml[i .. i + __tag_end]; i = i + __tag_end + 6; } } else if (strings.eql(inner_tag_name_or_tag_end, "LastModified")) { if (strings.indexOf(xml[i..], "")) |__tag_end| { last_modified = xml[i .. i + __tag_end]; i = i + __tag_end + 15; } } else if (strings.eql(inner_tag_name_or_tag_end, "Size")) { if (strings.indexOf(xml[i..], "")) |__tag_end| { const size = xml[i .. i + __tag_end]; object_size = std.fmt.parseInt(i64, size, 10) catch null; i = i + __tag_end + 7; } } else if (strings.eql(inner_tag_name_or_tag_end, "StorageClass")) { if (strings.indexOf(xml[i..], "")) |__tag_end| { storage_class = xml[i .. i + __tag_end]; i = i + __tag_end + 15; } } else if (strings.eql(inner_tag_name_or_tag_end, "ChecksumType")) { if (strings.indexOf(xml[i..], "")) |__tag_end| { checksum_type = xml[i .. i + __tag_end]; i = i + __tag_end + 15; } } else if (strings.eql(inner_tag_name_or_tag_end, "ChecksumAlgorithm")) { if (strings.indexOf(xml[i..], "")) |__tag_end| { checksum_algorithme = xml[i .. i + __tag_end]; i = i + __tag_end + 20; } } else if (strings.eql(inner_tag_name_or_tag_end, "ETag")) { if (strings.indexOf(xml[i..], "")) |__tag_end| { const input = xml[i .. i + __tag_end]; const size = std.mem.replacementSize(u8, input, """, "\""); var output = try bun.default_allocator.alloc(u8, size); const len = std.mem.replace(u8, input, """, "\"", output); if (len != 0) { etag = output[0 .. input.len - len * 5]; // 5 = """.len - 1 for replacement " } else { etag = input; } i = i + __tag_end + 7; } } else if (strings.eql(inner_tag_name_or_tag_end, "Owner")) { if (strings.indexOf(xml[i..], "")) |__tag_end| { const owner = xml[i .. i + __tag_end]; i = i + __tag_end + 8; if (strings.indexOf(owner, "")) |id_start| { const id_start_pos = id_start + 4; if (strings.indexOf(owner, "")) |id_end| { const isNotEmpty = id_start_pos < id_end; if (isNotEmpty) { owner_id = owner[id_start_pos..id_end]; } } } if (strings.indexOf(owner, "")) |id_start| { const id_start_pos = id_start + 13; if (strings.indexOf(owner, "")) |id_end| { const isNotEmpty = id_start_pos < id_end; if (isNotEmpty) { owner_display_name = owner[id_start_pos..id_end]; } } } } } else if (strings.eql(inner_tag_name_or_tag_end, "RestoreStatus")) { if (strings.indexOf(xml[i..], "")) |__tag_end| { const restore_status = xml[i .. i + __tag_end]; i = i + __tag_end + 16; if (strings.indexOf(restore_status, "")) |start| { const start_pos = start + 21; if (strings.indexOf(restore_status, "")) |_end| { const isNotEmpty = start_pos < _end; if (isNotEmpty) { const is_restore_in_progress_string = restore_status[start_pos.._end]; if (strings.eql(is_restore_in_progress_string, "true")) { is_restore_in_progress = true; } else if (strings.eql(is_restore_in_progress_string, "false")) { is_restore_in_progress = false; } } } } if (strings.indexOf(restore_status, "")) |start| { const start_pos = start + 19; if (strings.indexOf(restore_status, "")) |_end| { const isNotEmpty = start_pos < _end; if (isNotEmpty) { restore_expiry_date = restore_status[start_pos.._end]; } } } } } } else { // char is not > i += 1; } } else { // char is not < i += 1; } } if (object_key) |object_key_val| { var owner: ?ObjectOwner = null; if (owner_id != null or owner_display_name != null) { owner = .{ .id = owner_id, .display_name = owner_display_name, }; } var restore_status: ?ObjectRestoreStatus = null; if (is_restore_in_progress != null or restore_expiry_date != null) { restore_status = .{ .is_restore_in_progress = is_restore_in_progress, .restore_expiry_date = restore_expiry_date, }; } try contents.append(.{ .key = object_key_val, .etag = etag, .checksum_type = checksum_type, .checksum_algorithme = checksum_algorithme, .last_modified = last_modified, .object_size = object_size, .storage_class = storage_class, .owner = owner, .restore_status = restore_status, }); } } else if (strings.eql(tagName, "Name")) { if (strings.indexOf(xml[i..], "")) |_end| { result.name = xml[i .. i + _end]; i = i + _end; } } else if (strings.eql(tagName, "Delimiter")) { if (strings.indexOf(xml[i..], "")) |_end| { result.delimiter = xml[i .. i + _end]; i = i + _end; } } else if (strings.eql(tagName, "NextContinuationToken")) { if (strings.indexOf(xml[i..], "")) |_end| { result.next_continuation_token = xml[i .. i + _end]; i = i + _end; } } else if (strings.eql(tagName, "ContinuationToken")) { if (strings.indexOf(xml[i..], "")) |_end| { result.continuation_token = xml[i .. i + _end]; i = i + _end; } } else if (strings.eql(tagName, "StartAfter")) { if (strings.indexOf(xml[i..], "")) |_end| { result.start_after = xml[i .. i + _end]; i = i + _end; } } else if (strings.eql(tagName, "EncodingType")) { if (strings.indexOf(xml[i..], "")) |_end| { result.encoding_type = xml[i .. i + _end]; i = i + _end; } } else if (strings.eql(tagName, "KeyCount")) { if (strings.indexOf(xml[i..], "")) |_end| { const key_count = xml[i .. i + _end]; result.key_count = std.fmt.parseInt(i64, key_count, 10) catch null; i = i + _end; } } else if (strings.eql(tagName, "MaxKeys")) { if (strings.indexOf(xml[i..], "")) |_end| { const max_keys = xml[i .. i + _end]; result.max_keys = std.fmt.parseInt(i64, max_keys, 10) catch null; i = i + _end; } } else if (strings.eql(tagName, "Prefix")) { if (strings.indexOf(xml[i..], "")) |_end| { const prefix = xml[i .. i + _end]; if (prefix.len != 0) { result.prefix = prefix; } i = i + _end; } } else if (strings.eql(tagName, "IsTruncated")) { if (strings.indexOf(xml[i..], "")) |_end| { const is_truncated = xml[i .. i + _end]; if (strings.eql(is_truncated, "true")) { result.is_truncated = true; } else if (strings.eql(is_truncated, "false")) { result.is_truncated = false; } i = i + _end; } } else if (strings.eql(tagName, "CommonPrefixes")) { if (strings.indexOf(xml[i..], "")) |_end| { const common_prefixes_string = xml[i .. i + _end]; i = i + _end; var j: usize = 0; while (j < common_prefixes_string.len) { if (strings.indexOf(common_prefixes_string[j..], "")) |start| { j = j + start + 8; if (strings.indexOf(common_prefixes_string[j..], "")) |__end| { try common_prefixes.append(common_prefixes_string[j .. j + __end]); j = j + __end; } } else { break; } } } } } else { i += 1; } } if (contents.items.len != 0) { result.contents = contents; } else { contents.deinit(); } if (common_prefixes.items.len != 0) { result.common_prefixes = common_prefixes; } else { common_prefixes.deinit(); } } return result; } pub fn getListObjectsOptionsFromJS(globalThis: *jsc.JSGlobalObject, listOptions: JSValue) !S3ListObjectsOptions { var listObjectsOptions: S3ListObjectsOptions = .{ .continuation_token = null, .delimiter = null, .encoding_type = null, .fetch_owner = null, .max_keys = null, .prefix = null, .start_after = null, ._continuation_token = null, ._delimiter = null, ._encoding_type = null, ._prefix = null, ._start_after = null, }; if (!listOptions.isObject()) { return listObjectsOptions; } if (try listOptions.getTruthyComptime(globalThis, "continuationToken")) |val| { if (val.isString()) { const str = try bun.String.fromJS(val, globalThis); if (str.tag != .Empty and str.tag != .Dead) { listObjectsOptions._continuation_token = str.toUTF8(bun.default_allocator); listObjectsOptions.continuation_token = listObjectsOptions._continuation_token.?.slice(); } } } if (try listOptions.getTruthyComptime(globalThis, "delimiter")) |val| { if (val.isString()) { const str = try bun.String.fromJS(val, globalThis); if (str.tag != .Empty and str.tag != .Dead) { listObjectsOptions._delimiter = str.toUTF8(bun.default_allocator); listObjectsOptions.delimiter = listObjectsOptions._delimiter.?.slice(); } } } if (try listOptions.getTruthyComptime(globalThis, "encodingType")) |val| { if (val.isString()) { const str = try bun.String.fromJS(val, globalThis); if (str.tag != .Empty and str.tag != .Dead) { listObjectsOptions._encoding_type = str.toUTF8(bun.default_allocator); listObjectsOptions.encoding_type = listObjectsOptions._encoding_type.?.slice(); } } } if (try listOptions.getBooleanLoose(globalThis, "fetchOwner")) |val| { listObjectsOptions.fetch_owner = val; } if (try listOptions.getTruthyComptime(globalThis, "maxKeys")) |val| { if (val.isNumber()) { listObjectsOptions.max_keys = val.toInt32(); } } if (try listOptions.getTruthyComptime(globalThis, "prefix")) |val| { if (val.isString()) { const str = try bun.String.fromJS(val, globalThis); if (str.tag != .Empty and str.tag != .Dead) { listObjectsOptions._prefix = str.toUTF8(bun.default_allocator); listObjectsOptions.prefix = listObjectsOptions._prefix.?.slice(); } } } if (try listOptions.getTruthyComptime(globalThis, "startAfter")) |val| { if (val.isString()) { const str = try bun.String.fromJS(val, globalThis); if (str.tag != .Empty and str.tag != .Dead) { listObjectsOptions._start_after = str.toUTF8(bun.default_allocator); listObjectsOptions.start_after = listObjectsOptions._start_after.?.slice(); } } } return listObjectsOptions; } const std = @import("std"); const bun = @import("bun"); const strings = bun.strings; const jsc = bun.jsc; const JSGlobalObject = jsc.JSGlobalObject; const JSValue = jsc.JSValue;