// Copyright Epic Games, Inc. All Rights Reserved. "use strict"; import { ZenPage } from "./page.js" import { Fetcher } from "../util/fetcher.js" import { Friendly } from "../util/friendly.js" import { Table, PropTable, Toolbar, ProgressBar } from "../util/widgets.js" import { create_indexer } from "../indexer/indexer.js" //////////////////////////////////////////////////////////////////////////////// export class Page extends ZenPage { main() { this.set_title("oplog entry"); const project = this.get_param("project"); const oplog = this.get_param("oplog"); const opkey = this.get_param("opkey"); this._entry = new Fetcher() .resource("prj", project, "oplog", oplog, "entries") .param("opkey", opkey) .cbo(); this._indexer = this.load_indexer(project, oplog); this._files_index_start = Number(this.get_param("files_start", 0)) || 0; this._files_index_count = Number(this.get_param("files_count", 50)) || 0; this._build_page(); } async load_indexer(project, oplog, loaded_cb) { const progress_bar = this.add_widget(ProgressBar); progress_bar.set_progress("indexing"); const indexer = await create_indexer(project, oplog, (...args) => { progress_bar.set_progress(...args); }); progress_bar.destroy(); return indexer; } _build_deps(section, tree) { const project = this.get_param("project"); const oplog = this.get_param("oplog"); for (const dep_name in tree) { const dep_section = section.add_section(dep_name); const table = dep_section.add_widget(Table, ["name", "id"], Table.Flag_PackRight); for (const dep_id of tree[dep_name]) { const hex_id = dep_id.toString(16).padStart(16, "0"); const cell_values = ["loading...", hex_id]; const row = table.add_row(...cell_values); // Asynchronously resolve the name this._resolve_dep_name(row.get_cell(0), dep_id, project, oplog); } } } async _resolve_dep_name(cell, dep_id, project, oplog) { const indexer = await this._indexer; const opkey = indexer.lookup_id(dep_id); if (opkey) { cell.text(opkey).on_click((k) => this.view_opkey(k), opkey); } } _find_iohash_field(container, name) { const found_field = container.find(name); if (found_field != undefined) { var found_value = found_field.as_value(); if (found_value instanceof Uint8Array) { var ret = ""; for (var x of found_value) ret += x.toString(16).padStart(2, "0"); return ret; } } return null; } _is_null_io_hash_string(io_hash) { if (!io_hash) return true; for (let char of io_hash) { if (char != '0') { return false; } } return true; } async _build_meta(section, entry) { var tree = {} for (const field of entry) { var visibleKey = undefined; const name = field.get_name(); if (name == "CookPackageArtifacts") { visibleKey = name; } else if (name.startsWith("meta.")) { visibleKey = name.slice(5); } if (visibleKey != undefined) { var found_value = field.as_value(); if (found_value instanceof Uint8Array) { var ret = ""; for (var x of found_value) ret += x.toString(16).padStart(2, "0"); tree[visibleKey] = ret; } } } if (Object.keys(tree).length == 0) return; const sub_section = section.add_section("meta"); const table = sub_section.add_widget( Table, ["name", "actions"], Table.Flag_PackRight ); for (const key in tree) { const row = table.add_row(key); const value = tree[key]; const project = this.get_param("project"); const oplog = this.get_param("oplog"); const opkey = this.get_param("opkey"); const link = row.get_cell(0).link( (key === "cook.artifacts") ? `?page=cookartifacts&project=${project}&oplog=${oplog}&opkey=${opkey}&hash=${value}` : "/" + ["prj", project, "oplog", oplog, value+".json"].join("/") ); const action_tb = new Toolbar(row.get_cell(-1), true); // Add "view-raw" button for cook.artifacts if (key === "cook.artifacts") { action_tb.left().add("view-raw").on_click(() => { window.location = "/" + ["prj", project, "oplog", oplog, value+".json"].join("/"); }); } action_tb.left().add("copy-hash").on_click(async (v) => { await navigator.clipboard.writeText(v); }, value); } } async _build_page() { var entry = await this._entry; // Check if entry exists if (!entry || entry.as_object().find("entry") == null) { const opkey = this.get_param("opkey"); var section = this.add_section("Entry Not Found"); section.tag("p").text(`The entry "${opkey}" is not present in this dataset.`); section.tag("p").text("This could mean:"); const list = section.tag("ul"); list.tag("li").text("The entry is for an instance defined in code"); list.tag("li").text("The entry has not been added to the oplog yet"); list.tag("li").text("The entry key is misspelled"); list.tag("li").text("The entry was removed or never existed"); return; } entry = entry.as_object().find("entry").as_object(); const name = entry.find("key").as_value(); var section = this.add_section(name); var has_package_data = false; // tree { var tree = entry.find("$tree"); if (tree == undefined) tree = this._convert_legacy_to_tree(entry); if (tree != undefined) { delete tree["$id"]; if (Object.keys(tree).length != 0) { const sub_section = section.add_section("dependencies"); this._build_deps(sub_section, tree); } has_package_data = true; } } // meta if (has_package_data) { this._build_meta(section, entry); } // data if (has_package_data) { const sub_section = section.add_section("data"); const table = sub_section.add_widget( Table, ["name", "size", "rawsize", "actions"], Table.Flag_PackRight ); table.id("datatable"); for (const field_name of ["packagedata", "bulkdata"]) { var pkg_data = entry.find(field_name); if (pkg_data == undefined) continue; for (const item of pkg_data.as_array()) { var io_hash = undefined, size = undefined, raw_size = undefined, file_name = undefined; for (const field of item.as_object()) { if (field.is_named("data")) io_hash = field.as_value(); else if (field.is_named("filename")) file_name = field.as_value(); else if (field.is_named("size")) size = field.as_value(); else if (field.is_named("rawsize")) raw_size = field.as_value(); } if (io_hash instanceof Uint8Array) { var ret = ""; for (var x of io_hash) ret += x.toString(16).padStart(2, "0"); io_hash = ret; } size = (size !== undefined) ? Friendly.kib(size) : ""; raw_size = (raw_size !== undefined) ? Friendly.kib(raw_size) : ""; const row = table.add_row(file_name, size, raw_size); var base_name = file_name.split("/").pop().split("\\").pop(); const project = this.get_param("project"); const oplog = this.get_param("oplog"); const link = row.get_cell(0).link( "/" + ["prj", project, "oplog", oplog, io_hash].join("/") ); link.first_child().attr("download", `${io_hash}_${base_name}`); const action_tb = new Toolbar(row.get_cell(-1), true); action_tb.left().add("copy-hash").on_click(async (v) => { await navigator.clipboard.writeText(v); }, io_hash); } } } // files var has_file_data = false; { var file_data = entry.find("files"); if (file_data != undefined) { has_file_data = true; // Extract files into array this._files_data = []; for (const item of file_data.as_array()) { var io_hash = undefined, cid = undefined, server_path = undefined, client_path = undefined; for (const field of item.as_object()) { if (field.is_named("data")) io_hash = field.as_value(); else if (field.is_named("id")) cid = field.as_value(); else if (field.is_named("serverpath")) server_path = field.as_value(); else if (field.is_named("clientpath")) client_path = field.as_value(); } if (io_hash instanceof Uint8Array) { var ret = ""; for (var x of io_hash) ret += x.toString(16).padStart(2, "0"); io_hash = ret; } if (cid instanceof Uint8Array) { var ret = ""; for (var x of cid) ret += x.toString(16).padStart(2, "0"); cid = ret; } this._files_data.push({ server_path: server_path, client_path: client_path, io_hash: io_hash, cid: cid }); } this._files_index_max = this._files_data.length; const sub_section = section.add_section("files"); this._build_files_nav(sub_section); this._files_table = sub_section.add_widget( Table, ["name", "actions"], Table.Flag_PackRight ); this._files_table.id("filetable"); this._build_files_table(this._files_index_start); } } // props if (has_package_data) { const object = entry.to_js_object(); var sub_section = section.add_section("props"); sub_section.add_widget(PropTable).add_object(object); } if (!has_package_data && !has_file_data) return this._display_unsupported(section, entry); } _display_unsupported(section, entry) { const replacer = (key, value) => typeof value === "bigint" ? { $bigint: value.toString() } : value; const object = entry.to_js_object(); const text = JSON.stringify(object, replacer, " "); section.tag("pre").text(text); } _convert_legacy_to_tree(entry) { const raw_pkgst_entry = entry.find("packagestoreentry"); if (raw_pkgst_entry == undefined) //if there is no packagestorentry then don't show the fancy webpage, just show the raw json return; const tree = {}; const pkg_data = entry.find("packagedata"); if (pkg_data) { var id = 0n; for (var item of pkg_data.as_array()) { var pkg_id = item.as_object().find("id"); if (pkg_id == undefined) continue; pkg_id = pkg_id.as_value().subarray(0, 8); for (var i = 7; i >= 0; --i) { id <<= 8n; id |= BigInt(pkg_id[i]); } break; } tree["$id"] = id; } const pkgst_entry = raw_pkgst_entry.as_object(); for (const field of pkgst_entry) { const field_name = field.get_name(); if (field_name.endsWith("importedpackageids")) { var dep_name = field_name.slice(0, -18); if (dep_name.length == 0) dep_name = "hard"; else dep_name = "hard." + dep_name; var out = tree[dep_name] = []; for (var item of field.as_array()) out.push(item.as_value(BigInt)); } else if (field_name.endsWith("softpackagereferences")) { var dep_name = field_name.slice(0, -21); if (dep_name.length == 0) dep_name = "soft"; else dep_name = "soft." + dep_name; var out = tree[dep_name] = []; for (var item of field.as_array()) out.push(item.as_value(BigInt)); } } return tree; } view_opkey(opkey) { const params = this._params; params.set("opkey", opkey); window.location.search = params; } _build_files_nav(section) { const nav = section.add_widget(Toolbar); const left = nav.left(); left.add("|<") .on_click(() => this._on_files_next_prev(-10e10)); left.add("<<") .on_click(() => this._on_files_next_prev(-10)); left.add("prev").on_click(() => this._on_files_next_prev( -1)); left.add("next").on_click(() => this._on_files_next_prev( 1)); left.add(">>") .on_click(() => this._on_files_next_prev( 10)); left.add(">|") .on_click(() => this._on_files_next_prev( 10e10)); left.sep(); for (var count of [10, 25, 50, 100]) { var handler = (n) => this._on_files_change_count(n); left.add(count).on_click(handler, count); } const right = nav.right(); right.add(Friendly.sep(this._files_index_max)); right.sep(); var search_input = right.add("search:", "label").tag("input"); search_input.on("change", (x) => this._search_files(x.inner().value), search_input); } _build_files_table(index) { this._files_index_count = Math.max(this._files_index_count, 1); index = Math.min(index, this._files_index_max - this._files_index_count); index = Math.max(index, 0); const project = this.get_param("project"); const oplog = this.get_param("oplog"); const end_index = Math.min(index + this._files_index_count, this._files_index_max); this._files_table.clear(index); for (var i = index; i < end_index; i++) { const file_item = this._files_data[i]; const row = this._files_table.add_row(file_item.server_path); var base_name = file_item.server_path.split("/").pop().split("\\").pop(); if (this._is_null_io_hash_string(file_item.io_hash)) { const link = row.get_cell(0).link( "/" + ["prj", project, "oplog", oplog, file_item.cid].join("/") ); link.first_child().attr("download", `${file_item.cid}_${base_name}`); const action_tb = new Toolbar(row.get_cell(-1), true); action_tb.left().add("copy-id").on_click(async (v) => { await navigator.clipboard.writeText(v); }, file_item.cid); } else { const link = row.get_cell(0).link( "/" + ["prj", project, "oplog", oplog, file_item.io_hash].join("/") ); link.first_child().attr("download", `${file_item.io_hash}_${base_name}`); const action_tb = new Toolbar(row.get_cell(-1), true); action_tb.left().add("copy-hash").on_click(async (v) => { await navigator.clipboard.writeText(v); }, file_item.io_hash); } } this.set_param("files_start", index); this.set_param("files_count", this._files_index_count); this._files_index_start = index; } _on_files_change_count(value) { this._files_index_count = parseInt(value); this._build_files_table(this._files_index_start); } _on_files_next_prev(direction) { var index = this._files_index_start + (this._files_index_count * direction); index = Math.max(0, index); this._build_files_table(index); } _search_files(needle) { if (needle.length == 0) { this._build_files_table(this._files_index_start); return; } needle = needle.trim().toLowerCase(); this._files_table.clear(this._files_index_start); const project = this.get_param("project"); const oplog = this.get_param("oplog"); var added = 0; const truncate_at = this.get_param("searchmax") || 250; for (const file_item of this._files_data) { if (!file_item.server_path.toLowerCase().includes(needle)) continue; const row = this._files_table.add_row(file_item.server_path); var base_name = file_item.server_path.split("/").pop().split("\\").pop(); if (this._is_null_io_hash_string(file_item.io_hash)) { const link = row.get_cell(0).link( "/" + ["prj", project, "oplog", oplog, file_item.cid].join("/") ); link.first_child().attr("download", `${file_item.cid}_${base_name}`); const action_tb = new Toolbar(row.get_cell(-1), true); action_tb.left().add("copy-id").on_click(async (v) => { await navigator.clipboard.writeText(v); }, file_item.cid); } else { const link = row.get_cell(0).link( "/" + ["prj", project, "oplog", oplog, file_item.io_hash].join("/") ); link.first_child().attr("download", `${file_item.io_hash}_${base_name}`); const action_tb = new Toolbar(row.get_cell(-1), true); action_tb.left().add("copy-hash").on_click(async (v) => { await navigator.clipboard.writeText(v); }, file_item.io_hash); } if (++added >= truncate_at) { this._files_table.add_row("...truncated"); break; } } } }