refactor(domain): one owner each for non-empty text schemas and UTF-8 head excerpts (#10569)

Fifteen domain schema files each declared z.string().min(1), eight of them
as boundedTextSchema although it bounds nothing. They now share
nonEmptyTextSchema from src/domain/textSchemas.ts; the one trimming copy in
reconstructionCoverage.ts is renamed trimmedTextSchema.

Three private UTF-8 prefix truncators (gdb recent MI records, rizin
diagnostics, environment replay) and src/process/boundedUtf8Text.ts are
replaced by utf8HeadExcerpt in src/domain/utf8HeadExcerpt.ts, which encodes
only the retained prefix and returns a slice of the source text.
This commit is contained in:
morluto authored and GitHub committed 2026-11-14 13:28:28 -05:00
1 parent 1ba93b4602
commit eead7a5815
29 files changed
+236 -173

No files matched your search

@@ -1,6 +1,6 @@
import type { ComputerAction } from "../../domain/desktop/computerActions.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import { boundedUtf8Text } from "../../process/boundedUtf8Text.js";
import { utf8HeadExcerpt } from "../../domain/utf8HeadExcerpt.js";
/** Bytes the log keeps of each string in an action's arguments, such as type's text. */
export const LOGGED_TEXT_BYTES = 1024;
@@ -35,7 +35,7 @@ const bounded = (
redact: Redact,
): { readonly value: JsonValue; readonly cut: boolean } => {
if (typeof value === "string") {
const { text, truncated } = boundedUtf8Text(
const { text, truncated } = utf8HeadExcerpt(
redact(value),
LOGGED_TEXT_BYTES,
);
@@ -19,6 +19,7 @@ import {
type EnvironmentSource,
type RecordedExchange,
} from "./environmentSource.js";
import { utf8HeadExcerpt } from "../../domain/utf8HeadExcerpt.js";
/** Caller arguments as the MCP tool receives them. */
export type ReplayArguments = Readonly<Record<string, unknown>>;
@@ -307,13 +308,6 @@ export const selectsRecordedLine = (
return true;
};
/** The longest prefix of text whose UTF-8 encoding fits the byte limit, encoding only that prefix. */
const utf8Prefix = (text: string, maxBytes: number): string => {
// encodeInto stops before the first character that does not fit, so `read` ends on a boundary.
const { read } = new TextEncoder().encodeInto(text, new Uint8Array(maxBytes));
return text.slice(0, read);
};
/** Base64 characters, in whole groups, whose bytes fit the replay limit. */
const BASE64_LIMIT_CHARS = Math.floor(REPLAY_BODY_LIMIT_BYTES / 3) * 4;
@@ -334,7 +328,7 @@ const replayBody = (exchange: RecordedExchange): ReplayBody => {
: { body: body.encoded.slice(0, BASE64_LIMIT_CHARS), truncated: true };
case "cut":
return {
body: utf8Prefix(body.text, REPLAY_BODY_LIMIT_BYTES),
body: utf8HeadExcerpt(body.text, REPLAY_BODY_LIMIT_BYTES).text,
truncated: true,
};
case "json":
@@ -380,15 +374,18 @@ export const replayAnswer = (
exact: boolean,
{ body, truncated }: ReplayBody = replayBody(exchange),
): ReplayResult => {
const recordedPath = utf8Prefix(exchange.path, REPLAY_METADATA_LIMIT_BYTES);
const recordedPath = utf8HeadExcerpt(
exchange.path,
REPLAY_METADATA_LIMIT_BYTES,
).text;
const recordedQuery =
exchange.query === null
? null
: utf8Prefix(exchange.query, REPLAY_METADATA_LIMIT_BYTES);
: utf8HeadExcerpt(exchange.query, REPLAY_METADATA_LIMIT_BYTES).text;
const contentType =
exchange.contentType === null
? null
: utf8Prefix(exchange.contentType, REPLAY_METADATA_LIMIT_BYTES);
: utf8HeadExcerpt(exchange.contentType, REPLAY_METADATA_LIMIT_BYTES).text;
return {
status: exchange.status,
content_type: contentType,
+10 -10
View File
@@ -25,8 +25,8 @@ import { evidenceSchema, parseEvidence, type Evidence } from "./evidence.js";
import { offsetText } from "./firmware/espImageStructures.js";
import { jsonValueSchema } from "./jsonValue.js";
import { prefixedDigestSchema } from "./../domain/digests.js";
import { nonEmptyTextSchema } from "./textSchemas.js";
const textSchema = z.string().min(1);
const evidenceIdSchema = prefixedDigestSchema("ev");
const artifactIdSchema = prefixedDigestSchema("art");
@@ -38,22 +38,22 @@ const inspectionObservationSchema = z.strictObject({
"container-header",
"reconstruction",
]),
subject: textSchema,
subject: nonEmptyTextSchema,
value: jsonValueSchema,
evidence_id: evidenceIdSchema,
});
const inspectionHypothesisSchema = z.strictObject({
hypothesis_id: prefixedDigestSchema("aih"),
statement: textSchema,
statement: nonEmptyTextSchema,
confidence: z.enum(["medium", "low"]),
basis_evidence_ids: z.array(evidenceIdSchema).min(1),
limitation: textSchema,
limitation: nonEmptyTextSchema,
});
const inspectionContradictionBaseSchema = z.strictObject({
contradiction_id: prefixedDigestSchema("aic"),
statement: textSchema,
statement: nonEmptyTextSchema,
occurrence_id: prefixedDigestSchema("occ"),
evidence_id: evidenceIdSchema,
@@ -66,8 +66,8 @@ const inspectionContradictionSchema = z.union([
]);
const nextProbeSchema = z.strictObject({
operation: textSchema,
rationale: textSchema,
operation: nonEmptyTextSchema,
rationale: nonEmptyTextSchema,
authority: z.enum([
"artifact-bytes",
"managed-static-analysis",
@@ -79,7 +79,7 @@ const nextProbeSchema = z.strictObject({
const unexploredBranchSchema = z.strictObject({
branch_id: prefixedDigestSchema("aib"),
reason: z.enum(["format-specific-analysis-required", "unknown-format"]),
detail: textSchema,
detail: nonEmptyTextSchema,
next_probe: nextProbeSchema.nullable(),
evidence_id: evidenceIdSchema,
});
@@ -91,7 +91,7 @@ export const artifactInspectionResultSchema = z.strictObject({
manifest_id: prefixedDigestSchema("agm"),
root_artifact_id: artifactIdSchema,
root_sha256: z.string().regex(/^[a-f0-9]{64}$/u),
root_format: textSchema,
root_format: nonEmptyTextSchema,
}),
substeps: z
.array(
@@ -125,7 +125,7 @@ export const artifactInspectionResultSchema = z.strictObject({
next_probes_omitted: z.int().min(0),
}),
evidence_links: z.array(evidenceIdSchema).min(1),
limitations: z.array(textSchema),
limitations: z.array(nonEmptyTextSchema),
});
export type ArtifactInspectionResult = z.infer<
@@ -1,9 +1,8 @@
import { z } from "zod";
import { javascriptSourceRangeSchema } from "./javascriptSourceCoordinates.js";
import { digestSchema, prefixedDigestSchema } from "../digests.js";
import { nonEmptyTextSchema } from "../textSchemas.js";
const boundedTextSchema = z.string().min(1);
const boundedKeySchema = z.string().min(1);
const unsafePathSegments = new Set(["", ".", ".."]);
const isSafeRelativePath = (path: string): boolean => {
if (path.startsWith("/")) return false;
@@ -63,7 +62,7 @@ const applicationUnavailableReasonSchema = z.enum([
const unavailableFactSchema = z.strictObject({
available: z.literal(false),
reason: applicationUnavailableReasonSchema,
detail: boundedTextSchema,
detail: nonEmptyTextSchema,
});
const availableArtifactSchema = z
@@ -89,7 +88,7 @@ const applicationArtifactReferenceSchema = z.discriminatedUnion("available", [
const sourceRangeSchema = javascriptSourceRangeSchema.safeExtend({
kind: z.literal("source-range"),
source: boundedTextSchema,
source: nonEmptyTextSchema,
});
const fileOffsetRangeSchema = z
@@ -122,19 +121,19 @@ const applicationLocationSchema = z.discriminatedUnion("kind", [
z.strictObject({
kind: z.literal("runtime"),
capture_sha256: digestSchema,
target_key: boundedKeySchema,
frame_key: boundedKeySchema.nullable(),
script_key: boundedKeySchema.nullable(),
target_key: nonEmptyTextSchema,
frame_key: nonEmptyTextSchema.nullable(),
script_key: nonEmptyTextSchema.nullable(),
}),
z.strictObject({
kind: z.literal("native-address"),
address: boundedKeySchema,
symbol: boundedTextSchema.nullable(),
address: nonEmptyTextSchema,
symbol: nonEmptyTextSchema.nullable(),
}),
z.strictObject({
kind: z.literal("package-export"),
package_name: boundedKeySchema,
export_path: boundedTextSchema,
package_name: nonEmptyTextSchema,
export_path: nonEmptyTextSchema,
}),
]);
@@ -247,7 +246,7 @@ const applicationGraphEvidenceBaseSchema = z.strictObject({
location: applicationGraphEvidenceLocationSchema,
extractor: applicationExtractorSchema,
coverage: applicationCoverageSchema,
limitations: z.array(boundedTextSchema),
limitations: z.array(nonEmptyTextSchema),
evidence_ids: z.array(prefixedDigestSchema("ev")),
});
@@ -439,7 +438,7 @@ export const applicationNodeIdentitySchema = z.discriminatedUnion("strategy", [
strategy: z.literal("artifact-local-key"),
stability: z.literal("artifact-version"),
artifact_sha256: digestSchema,
namespace: boundedKeySchema,
namespace: nonEmptyTextSchema,
// The exact artifact-local value, such as `""` from `fetch("")`.
key: z.string(),
}),
@@ -448,13 +447,13 @@ export const applicationNodeIdentitySchema = z.discriminatedUnion("strategy", [
stability: z.literal("capture-only"),
capture_evidence_id: prefixedDigestSchema("ev"),
capture_sha256: digestSchema,
runtime_key: boundedTextSchema,
runtime_key: nonEmptyTextSchema,
}),
z.strictObject({
strategy: z.literal("observation-fingerprint"),
stability: z.literal("observation-only"),
observation_sha256: digestSchema,
scope: boundedTextSchema,
scope: nonEmptyTextSchema,
}),
]);
@@ -8,8 +8,7 @@ import {
} from "./javascriptApplicationEvidenceSchemas.js";
import { jsonValueSchema } from "../jsonValue.js";
import { prefixedDigestSchema } from "../digests.js";
const boundedTextSchema = z.string().min(1);
import { nonEmptyTextSchema } from "../textSchemas.js";
/** An observed IPC channel match whose uniqueness is not established. */
export const IPC_COVERAGE_LIMITED_PAIRING_BASIS =
@@ -148,7 +147,7 @@ export const javascriptApplicationGraphInputSchema = z.strictObject({
nodes: z.array(applicationNodeSchema).min(1),
edges: z.array(applicationEdgeSchema),
coverage: applicationCoverageSchema,
limitations: z.array(boundedTextSchema),
limitations: z.array(nonEmptyTextSchema),
});
/** Strict stored shape for a JavaScript Application Graph. */
@@ -8,10 +8,10 @@ import {
} from "./javascriptApplicationGraph.js";
import { JAVASCRIPT_APPLICATION_NODE_KINDS } from "./javascriptApplicationGraphSchemas.js";
import { prefixedDigestSchema } from "../digests.js";
import { nonEmptyTextSchema } from "../textSchemas.js";
const evidenceIdSchema = prefixedDigestSchema("ev");
const nodeIdSchema = prefixedDigestSchema("jag_node");
const textSchema = z.string().min(1);
/** Check paired version identities and per-side native identity uniqueness. */
const validateApplicationVersionIdentities = (
@@ -128,7 +128,7 @@ const comparisonItemContextShape = {
]),
),
evidence_links: z.array(evidenceIdSchema).min(2),
limitations: z.array(textSchema),
limitations: z.array(nonEmptyTextSchema),
};
const comparisonItemSchema = z.union([
@@ -212,7 +212,7 @@ export const applicationVersionComparisonResultSchema = z
graph: javascriptApplicationGraphSchema,
coverage: comparisonCoverageSchema,
evidence_links: z.array(evidenceIdSchema).min(2),
limitations: z.array(textSchema),
limitations: z.array(nonEmptyTextSchema),
})
.superRefine((result, context) => {
const sourceGraphsComplete =
@@ -4,11 +4,11 @@ import { javascriptSourceRangeSchema } from "./javascriptSourceCoordinates.js";
import { evidenceSchema } from "../evidence.js";
import { prefixedDigestSchema } from "../digests.js";
import { nonEmptyTextSchema } from "../textSchemas.js";
const evidenceIdSchema = prefixedDigestSchema("ev");
const graphIdSchema = prefixedDigestSchema("jag");
const nodeIdSchema = prefixedDigestSchema("jag_node");
const textSchema = z.string().min(1);
const exportNameSchema = z.string();
const semanticPrimitiveSchema = z.union([
z.string(),
@@ -64,9 +64,9 @@ export const compareJavaScriptExportShapesInputSchema = z
.strictObject({
left: evidenceSchema,
right: evidenceSchema,
left_module_path: textSchema,
left_module_path: nonEmptyTextSchema,
left_export_name: exportNameSchema,
right_module_path: textSchema,
right_module_path: nonEmptyTextSchema,
right_export_name: exportNameSchema,
})
.superRefine(validateJavaScriptExportShapeIdentities);
@@ -92,7 +92,7 @@ const projectedReturnFieldSchema = z.discriminatedUnion("state", [
...projectedReturnFieldShape,
state: z.literal("unknown"),
value: z.null(),
reason: textSchema,
reason: nonEmptyTextSchema,
}),
]);
export type ProjectedReturnField = z.infer<typeof projectedReturnFieldSchema>;
@@ -149,9 +149,9 @@ const projectedReturnShapeSchema = z
export const projectedExportReturnShapesSchema = z
.strictObject({
semantic_role: z.literal("export-return-shapes"),
module_path: textSchema,
module_path: nonEmptyTextSchema,
exported_name: exportNameSchema,
callable_id: textSchema,
callable_id: nonEmptyTextSchema,
callable_kind: z.enum(["function", "class", "method"]),
static_return_shapes: z.array(projectedReturnShapeSchema),
return_shape_coverage: z.strictObject({
@@ -177,7 +177,7 @@ export const projectedExportReturnShapesSchema = z
const exportCandidateSchema = z.strictObject({
node_id: nodeIdSchema,
module_path: textSchema,
module_path: nonEmptyTextSchema,
export_name: exportNameSchema,
matches_requested_module: z.boolean(),
matches_requested_export: z.boolean(),
@@ -186,7 +186,7 @@ const exportCandidateSchema = z.strictObject({
const selectorResultShape = {
evidence_id: evidenceIdSchema,
graph_id: graphIdSchema,
requested_module_path: textSchema,
requested_module_path: nonEmptyTextSchema,
requested_export_name: exportNameSchema,
candidates: z.array(exportCandidateSchema),
omitted_candidates: z.int().min(0),
@@ -221,7 +221,7 @@ const valueAvailabilitySchema = z.discriminatedUnion("availability", [
}),
z.strictObject({
availability: z.literal("unknown"),
reason: textSchema,
reason: nonEmptyTextSchema,
}),
]);
@@ -251,7 +251,7 @@ export const javaScriptExportShapeComparisonChangeSchema = z.strictObject({
left_source_range: javascriptSourceRangeSchema.nullable(),
right_source_range: javascriptSourceRangeSchema.nullable(),
evidence_links: z.array(evidenceIdSchema).length(2),
limitations: z.array(textSchema),
limitations: z.array(nonEmptyTextSchema),
});
const inventoryPropertyCoverageSchema = z.strictObject({
@@ -311,7 +311,7 @@ export const javaScriptExportShapeComparisonResultSchema = z.strictObject({
omitted_changes: z.int().min(0),
}),
evidence_links: z.array(evidenceIdSchema).length(2),
limitations: z.array(textSchema),
limitations: z.array(nonEmptyTextSchema),
});
export type ProjectedExportReturnShapes = z.output<
@@ -3,11 +3,11 @@ import { z } from "zod";
import { evidenceSchema, providerSchema } from "../evidence.js";
import { javascriptApplicationGraphSchema } from "./javascriptApplicationGraph.js";
import { digestSchema, prefixedDigestSchema } from "../digests.js";
import { nonEmptyTextSchema } from "../textSchemas.js";
const evidenceIdSchema = prefixedDigestSchema("ev");
const nodeIdSchema = prefixedDigestSchema("jag_node");
const edgeIdSchema = prefixedDigestSchema("jag_edge");
const boundedTextSchema = z.string().min(1);
/** Matching mode for a literal seed: string seeds default to containment. */
export const featureSeedMatchMode = (seed: {
@@ -19,7 +19,7 @@ export const featureSeedMatchMode = (seed: {
const apiSeedSchema = z.union([
z.strictObject({
kind: z.literal("api"),
value: boundedTextSchema,
value: nonEmptyTextSchema,
match: z.enum(["exact", "contains"]).nullable().default(null),
case_sensitive: z.boolean().default(false),
}),
@@ -34,7 +34,7 @@ const apiSeedSchema = z.union([
const apiPathSeedSchema = z.union([
z.strictObject({
kind: z.literal("api"),
value: boundedTextSchema,
value: nonEmptyTextSchema,
match: z.enum(["exact", "contains"]).nullable().default(null),
case_sensitive: z.boolean().default(false),
entry_path: z.array(z.string()).min(1),
@@ -63,7 +63,7 @@ export const applicationFeatureSeedSchema = z.union([
"module",
"native-export",
]),
value: boundedTextSchema,
value: nonEmptyTextSchema,
match: z.enum(["exact", "contains"]).nullable().default(null),
case_sensitive: z.boolean().default(false),
}),
@@ -220,7 +220,7 @@ export const applicationFeatureTraceResultSchema = z.strictObject({
total_seed_matches: z.int().min(0),
}),
evidence_links: z.array(evidenceIdSchema).min(1),
limitations: z.array(boundedTextSchema),
limitations: z.array(nonEmptyTextSchema),
});
export type TraceApplicationFeatureInput = z.infer<
@@ -8,10 +8,10 @@ import { javascriptRuntimeUnresolvedLocationSchema } from "./javascriptRuntimeOb
import { javascriptApplicationGraphSchema } from "./javascriptApplicationGraph.js";
import { digestSchema, prefixedDigestSchema } from "../digests.js";
import { urlDeclaresQueryOrFragment } from "../urlComponents.js";
import { nonEmptyTextSchema } from "../textSchemas.js";
const evidenceIdSchema = prefixedDigestSchema("ev");
const nodeIdSchema = prefixedDigestSchema("jag_node");
const boundedTextSchema = z.string().min(1);
const safePrefixSchema = z
.string()
.refine(
@@ -171,7 +171,7 @@ const captureSummarySchema = z.strictObject({
capture_sha256: digestSchema,
kind: z.enum(["browser", "electron", "v8-inspector", "electron-active"]),
target_node_id: nodeIdSchema,
target_key: boundedTextSchema,
target_key: nonEmptyTextSchema,
target_location: z
.string()
.min(1)
@@ -282,7 +282,7 @@ export const javascriptRuntimeReconciliationResultSchema = z.strictObject({
used_for_primary_matching: z.literal(false),
static_layer_count: z.int().min(0),
runtime_script_declarations: z.int().min(0),
limitation: boundedTextSchema,
limitation: nonEmptyTextSchema,
}),
coverage: z.strictObject({
status: z.enum(["complete-within-inputs", "partial", "truncated"]),
@@ -293,7 +293,7 @@ export const javascriptRuntimeReconciliationResultSchema = z.strictObject({
omitted_graph_items: z.int().min(0),
}),
evidence_links: z.array(evidenceIdSchema).min(2),
limitations: z.array(boundedTextSchema),
limitations: z.array(nonEmptyTextSchema),
});
export type ReconcileJavaScriptRuntimeInput = z.infer<
@@ -9,8 +9,8 @@ import {
} from "./javascriptApplicationEvidenceSchemas.js";
import { jsonValueSchema } from "../jsonValue.js";
import { digestSchema, prefixedDigestSchema } from "../digests.js";
import { nonEmptyTextSchema } from "../textSchemas.js";
const boundedTextSchema = z.string().min(1);
const semanticNodeIdSchema = prefixedDigestSchema("jsrg_node");
const semanticRelationIdSchema = z
.string()
@@ -278,7 +278,7 @@ export const javaScriptSemanticUnknownInputSchema = z.strictObject({
family: z.enum(JAVASCRIPT_SEMANTIC_RELATION_FAMILIES),
relation_kinds: z.array(z.enum(JAVASCRIPT_SEMANTIC_RELATIONS)).min(1),
reason: z.enum(JAVASCRIPT_SEMANTIC_UNKNOWN_REASONS),
detail: boundedTextSchema,
detail: nonEmptyTextSchema,
candidate_node_ids: z.array(semanticNodeIdSchema),
evidence: applicationGraphEvidenceSchema,
});
@@ -315,7 +315,7 @@ export const javaScriptSemanticFingerprintInputSchema = z.strictObject({
algorithm: z.literal("rea.javascript-semantic-function"),
status: z.enum(["complete", "partial", "unavailable"]),
components: fingerprintComponentsSchema,
limitations: z.array(boundedTextSchema),
limitations: z.array(nonEmptyTextSchema),
evidence: applicationGraphEvidenceSchema,
});
@@ -364,7 +364,7 @@ export const javaScriptSemanticGraphInputSchema = z.strictObject({
fingerprints: z.array(javaScriptSemanticFingerprintSchema),
unknowns: z.array(javaScriptSemanticUnknownSchema),
coverage: graphCoverageSchema,
limitations: z.array(boundedTextSchema),
limitations: z.array(nonEmptyTextSchema),
});
/** Stored JavaScript Semantic Relation Graph. */
@@ -5,10 +5,10 @@ import { evidenceSchema } from "../evidence.js";
import { JAVASCRIPT_APPLICATION_NODE_KINDS } from "./javascriptApplicationGraphSchemas.js";
import { historicalSourceGraphSchema } from "../referenceSourceGraph.js";
import { digestSchema, prefixedDigestSchema } from "../digests.js";
import { nonEmptyTextSchema } from "../textSchemas.js";
const evidenceIdSchema = prefixedDigestSchema("ev");
const nodeIdSchema = prefixedDigestSchema("jag_node");
const textSchema = z.string().min(1);
export const SOURCE_TO_BUNDLE_SIGNAL_WEIGHTS = [
["exact-source-digest", 100],
@@ -39,8 +39,8 @@ export const compareSourceToBundleInputSchema = z.strictObject({
const sourceToBundleSignalSchema = z.strictObject({
kind: sourceToBundleSignalKindSchema,
weight: z.int().min(1).max(100),
source_value: textSchema,
current_values: z.array(textSchema).min(1),
source_value: nonEmptyTextSchema,
current_values: z.array(nonEmptyTextSchema).min(1),
});
const sourceToBundleCandidateSchema = z.strictObject({
@@ -55,9 +55,9 @@ const sourceToBundleCandidateSchema = z.strictObject({
const sourceToBundleItemSchema = z.strictObject({
mapping_id: prefixedDigestSchema("stbc_item"),
source_path: textSchema,
source_path: nonEmptyTextSchema,
source_sha256: digestSchema.nullable(),
source_language: textSchema.nullable(),
source_language: nonEmptyTextSchema.nullable(),
status: z.enum([
"unchanged",
"modified",
@@ -70,7 +70,7 @@ const sourceToBundleItemSchema = z.strictObject({
confidence: z.enum(["exact", "high", "medium", "unknown"]),
current_node_ids: z.array(nodeIdSchema),
candidates: z.array(sourceToBundleCandidateSchema),
limitations: z.array(textSchema),
limitations: z.array(nonEmptyTextSchema),
});
/** Deterministic, evidence-bearing historical-source to shipped-bundle comparison. */
@@ -120,7 +120,7 @@ export const sourceToBundleComparisonResultSchema = z.strictObject({
candidate_evaluations: z.int().min(0),
}),
evidence_links: z.array(evidenceIdSchema).length(1),
limitations: z.array(textSchema),
limitations: z.array(nonEmptyTextSchema),
});
export type SourceToBundleComparisonResult = z.infer<
@@ -42,9 +42,9 @@ import { AnalysisInputError } from "../analysisErrorCore.js";
import type { EvidenceIntegrityError } from "../evidenceErrors.js";
import { analysisInputErrorFromIssues } from "../inputIssueProjection.js";
import { err, ok, type Result } from "../result.js";
import { nonEmptyTextSchema } from "../textSchemas.js";
const evidenceIdSchema = prefixedDigestSchema("ev");
const textSchema = z.string().min(1);
const managedApplicationGraphReferenceShape = z.strictObject({
managed_artifact: evidenceInputSchema.optional(),
@@ -135,7 +135,7 @@ export const managedApplicationGraphResultSchema = z
status: z.enum(["complete-within-inputs", "partial"]),
}),
evidence_links: z.array(evidenceIdSchema).min(1).max(3),
limitations: z.array(textSchema),
limitations: z.array(nonEmptyTextSchema),
})
.superRefine((result, context) => {
const [rootId] = result.graph.root_node_ids;
@@ -25,9 +25,9 @@ import {
import { keyMembers } from "./managedMemberComparisonMatch.js";
import { hasSignatureResolution } from "./managedMemberSignatureIdentity.js";
import { digestSchema, prefixedDigestSchema } from "../digests.js";
import { nonEmptyTextSchema } from "../textSchemas.js";
const evidenceIdSchema = prefixedDigestSchema("ev");
const boundedTextSchema = z.string().min(1);
const memberComparisonShape = <Source extends z.ZodType>(source: Source) => ({
left: source,
@@ -133,7 +133,7 @@ const comparisonItemSchema = <
const comparisonItemContextShape = {
evidence_links: z.array(evidenceIdSchema).length(2),
limitations: z.array(boundedTextSchema),
limitations: z.array(nonEmptyTextSchema),
};
const methodComparisonItemSchema = comparisonItemSchema(
@@ -221,7 +221,7 @@ export const managedMemberComparisonResultSchema = z.strictObject({
right_status: z.enum(["complete", "partial", "unavailable"]),
}),
evidence_links: z.array(evidenceIdSchema).length(2),
limitations: z.array(boundedTextSchema),
limitations: z.array(nonEmptyTextSchema),
});
export type ManagedMemberComparisonResult = z.infer<
@@ -7,9 +7,9 @@ import {
managedNativeImportSchema,
} from "./managedArtifact.js";
import { digestSchema, prefixedDigestSchema } from "../digests.js";
import { nonEmptyTextSchema } from "../textSchemas.js";
const evidenceIdSchema = prefixedDigestSchema("ev");
const textSchema = z.string().min(1);
const nativeSymbolSchema = z.strictObject({
evidence_id: evidenceIdSchema,
@@ -43,7 +43,7 @@ const pinvokeVerificationContextShape = {
item_id: prefixedDigestSchema("mnv_pinvoke"),
managed: verifiedManagedDeclarationSchema,
evidence_links: z.array(evidenceIdSchema).min(1),
limitations: z.array(textSchema),
limitations: z.array(nonEmptyTextSchema),
};
const observedCandidatesSchema = z
@@ -182,11 +182,11 @@ export const managedNativeVerificationResultSchema = z
native_implementations: z.strictObject({
runtime_provided: z.int().nonnegative().nullable().default(null),
unresolved: z.int().min(0),
reason: textSchema,
reason: nonEmptyTextSchema,
}),
coverage: verificationCoverageSchema,
evidence_links: z.array(evidenceIdSchema).min(2),
limitations: z.array(textSchema),
limitations: z.array(nonEmptyTextSchema),
})
.superRefine((result, context) => {
const observedNativeCount =
+10 -10
View File
@@ -17,13 +17,13 @@ import { readManagedInspectionEvidence } from "./managedInspectionEvidence.js";
import { AnalysisInputError } from "../analysisErrorCore.js";
import { EvidenceIntegrityError } from "../evidenceErrors.js";
import { err, ok, type Result } from "../result.js";
import { nonEmptyTextSchema } from "../textSchemas.js";
const evidenceIdSchema = prefixedDigestSchema("ev");
const textSchema = z.string().min(1);
const decompilerSchema = z.strictObject({
name: textSchema,
version: textSchema.nullable().default(null),
name: nonEmptyTextSchema,
version: nonEmptyTextSchema.nullable().default(null),
family: z.enum(["ilspy", "dnspy", "dnlib", "mono-cecil", "other"]),
executable_sha256: digestSchema.nullable().default(null),
options: z.array(z.string()).default([]),
@@ -36,9 +36,9 @@ const managedReconstructionMethodInputSchema = z.strictObject({
reconstruction: z.strictObject({
kind: z.enum(["decompiled-csharp", "decompiled-il", "semantic-pseudocode"]),
language: z.enum(["csharp", "il", "pseudocode"]),
text: textSchema,
text: nonEmptyTextSchema,
text_sha256: digestSchema.optional(),
source_path: textSchema.nullable().optional(),
source_path: nonEmptyTextSchema.nullable().optional(),
start_line: z.int().min(1).nullable().optional(),
end_line: z.int().min(1).nullable().optional(),
}),
@@ -51,7 +51,7 @@ type ManagedReconstructionMethodInput = z.infer<
const managedReconstructionImportRequestShape = {
decompiler: decompilerSchema,
methods: z.array(managedReconstructionMethodInputSchema).min(1),
notes: z.array(textSchema).default([]),
notes: z.array(nonEmptyTextSchema).default([]),
};
const reconstructionInput = <Source extends z.ZodType>(source: Source) =>
@@ -79,11 +79,11 @@ const importedMethodSchema = z.strictObject({
reconstruction: z.strictObject({
kind: z.enum(["decompiled-csharp", "decompiled-il", "semantic-pseudocode"]),
language: z.enum(["csharp", "il", "pseudocode"]),
text: textSchema,
text: nonEmptyTextSchema,
text_sha256: digestSchema,
text_length: z.int().min(1),
line_count: z.int().min(1),
source_path: textSchema.nullable(),
source_path: nonEmptyTextSchema.nullable(),
start_line: z.int().min(1).nullable(),
end_line: z.int().min(1).nullable(),
}),
@@ -119,9 +119,9 @@ export const managedReconstructionImportResultSchema = z.strictObject({
total_text_bytes: z.int().min(1),
}),
methods: z.array(importedMethodSchema).min(1),
notes: z.array(textSchema),
notes: z.array(nonEmptyTextSchema),
evidence_links: z.array(evidenceIdSchema).length(1),
limitations: z.array(textSchema),
limitations: z.array(nonEmptyTextSchema),
});
export type ManagedReconstructionImportInput = z.infer<
+6 -6
View File
@@ -34,7 +34,7 @@ const stableIdSchema = z
export const reconstructionBoundaryIdSchema = stableIdSchema.describe(
"Exact identifier of a boundary declared in the reconstruction coverage.",
);
const boundedTextSchema = z.string().trim().min(1);
const trimmedTextSchema = z.string().trim().min(1);
const authoritySchema = z.enum([
"observed",
@@ -57,7 +57,7 @@ const surfaceSchema = z.strictObject({
family: z.string().trim().min(1),
artifact_id: stableIdSchema,
occurrence_id: z.string().trim().min(1).nullable(),
location: boundedTextSchema,
location: trimmedTextSchema,
authority: authoritySchema,
dependency_surface_ids: z.array(stableIdSchema),
evidence_ids: z.array(evidenceIdSchema).min(1),
@@ -65,7 +65,7 @@ const surfaceSchema = z.strictObject({
const implementationOwnerSchema = z.strictObject({
disposition: z.literal("implemented"),
owner_path: boundedTextSchema,
owner_path: trimmedTextSchema,
owner_export: z.string().trim().min(1).nullable(),
owner_sha256: digestSchema,
path_state: z.enum(["present", "missing", "unknown"]),
@@ -75,7 +75,7 @@ const implementationOwnerSchema = z.strictObject({
const externalOwnerSchema = z.strictObject({
disposition: z.enum(["external", "non-goal"]),
rationale: boundedTextSchema,
rationale: trimmedTextSchema,
});
const ownerSchema = z.strictObject({
@@ -284,7 +284,7 @@ const closureReasonSchema = z.strictObject({
"package-proof-unknown",
]),
subject_id: stableIdSchema,
detail: boundedTextSchema,
detail: trimmedTextSchema,
});
export const reconstructionClosureResultSchema = z.strictObject({
@@ -300,7 +300,7 @@ export const reconstructionClosureResultSchema = z.strictObject({
z.strictObject({
operation: stableIdSchema,
subject_id: stableIdSchema,
rationale: boundedTextSchema,
rationale: trimmedTextSchema,
}),
),
evidence_ids: z.array(evidenceIdSchema),
+6 -6
View File
@@ -10,7 +10,7 @@ import {
} from "./referenceSourcePathIdentity.js";
import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js";
import { digestSchema, prefixedDigestSchema } from "./digests.js";
const boundedTextSchema = z.string().min(1);
import { nonEmptyTextSchema } from "./textSchemas.js";
const relativePathSchema = z
.string()
.min(1)
@@ -38,9 +38,9 @@ const classificationSchema = z.enum([
const entryBaseShape = {
path: relativePathSchema,
classifications: z.array(classificationSchema).min(1),
limitations: z.array(boundedTextSchema),
limitations: z.array(nonEmptyTextSchema),
// Host presentation is retained evidence, separate from relocatable semantics.
local_diagnostics: z.array(boundedTextSchema).exactOptional(),
local_diagnostics: z.array(nonEmptyTextSchema).exactOptional(),
};
const sourceFileBaseShape = {
@@ -129,7 +129,7 @@ const sourceRelationshipSchema = z.strictObject({
const parseFailureSchema = z.strictObject({
path: relativePathSchema,
parser: z.string().min(1),
reason: boundedTextSchema,
reason: nonEmptyTextSchema,
});
const exclusionSchema = z.discriminatedUnion("reason", [
@@ -144,7 +144,7 @@ const exclusionSchema = z.discriminatedUnion("reason", [
"default-ignored",
"caller-excluded",
]),
pattern: boundedTextSchema,
pattern: nonEmptyTextSchema,
}),
z.strictObject({
path: relativePathSchema,
@@ -196,7 +196,7 @@ const graphShape = {
.min(1)
.regex(/^[\w .:@\x2f+\x2d]+$/u),
}),
limitations: z.array(boundedTextSchema),
limitations: z.array(nonEmptyTextSchema),
};
const historicalSourceGraphStructureSchema = z
+7
View File
@@ -0,0 +1,7 @@
import { z } from "zod";
/**
* A string with at least one UTF-16 code unit. It bounds nothing else: apply a
* real length limit where a format, protocol or resource requires one.
*/
export const nonEmptyTextSchema = z.string().min(1);
+61
View File
@@ -0,0 +1,61 @@
import { expect, it } from "vitest";
import { utf8HeadExcerpt } from "./utf8HeadExcerpt.js";
it("keeps text within the budget unchanged", () => {
expect(utf8HeadExcerpt("abc", 3)).toEqual({
text: "abc",
bytes: 3,
truncated: false,
});
expect(utf8HeadExcerpt("", 0)).toEqual({
text: "",
bytes: 0,
truncated: false,
});
});
it("keeps the start and never splits a character", () => {
// "é" is 2 bytes, "€" 3 and "😀" 4 (a surrogate pair).
expect(utf8HeadExcerpt("aé€z", 5)).toEqual({
text: "aé",
bytes: 3,
truncated: true,
});
expect(utf8HeadExcerpt("aé€z", 6)).toEqual({
text: "aé€",
bytes: 6,
truncated: true,
});
expect(utf8HeadExcerpt("😀x", 3)).toEqual({
text: "",
bytes: 0,
truncated: true,
});
expect(utf8HeadExcerpt("😀x", 4)).toEqual({
text: "😀",
bytes: 4,
truncated: true,
});
});
it("charges a lone surrogate as its 3-byte replacement and keeps the source unit", () => {
expect(utf8HeadExcerpt("\uD800ab", 4)).toEqual({
text: "\uD800a",
bytes: 4,
truncated: true,
});
});
it("returns nothing for a non-positive budget", () => {
expect(utf8HeadExcerpt("abc", 0)).toEqual({
text: "",
bytes: 0,
truncated: true,
});
expect(utf8HeadExcerpt("abc", -1)).toEqual({
text: "",
bytes: 0,
truncated: true,
});
});
+28
View File
@@ -0,0 +1,28 @@
/**
* The longest prefix of `text` whose UTF-8 encoding fits `maximumBytes`,
* never splitting a character. `text` is a slice of the source, `bytes` its
* UTF-8 length, and `truncated` says whether any of the end was dropped. Only
* the retained prefix is encoded.
*/
export const utf8HeadExcerpt = (
text: string,
maximumBytes: number,
): {
readonly text: string;
readonly bytes: number;
readonly truncated: boolean;
} => {
if (maximumBytes <= 0)
return { text: "", bytes: 0, truncated: text.length > 0 };
// A UTF-16 code unit encodes to at most three UTF-8 bytes, and encodeInto
// stops before the first character that does not fit.
const { read, written } = new TextEncoder().encodeInto(
text,
new Uint8Array(Math.min(maximumBytes, text.length * 3)),
);
return {
text: text.slice(0, read),
bytes: written,
truncated: read < text.length,
};
};
+2 -9
View File
@@ -57,6 +57,7 @@ import {
type ProcessStartTimeReader,
verifyOwnedInferiorsEnded,
} from "../process/ownedDebuggerSession.js";
import { utf8HeadExcerpt } from "../domain/utf8HeadExcerpt.js";
const STARTUP_TIMEOUT_MS = 10_000;
@@ -194,15 +195,7 @@ const truncateUtf8RecordPrefix = (line: string): string => {
const budget =
MAX_RECENT_MI_LINE_BYTES -
Buffer.byteLength(RECENT_RECORD_TRUNCATION_MARKER, "utf8");
let prefix = "";
let retainedBytes = 0;
for (const codePoint of line) {
const codePointBytes = Buffer.byteLength(codePoint, "utf8");
if (retainedBytes + codePointBytes > budget) break;
prefix += codePoint;
retainedBytes += codePointBytes;
}
return `${prefix}${RECENT_RECORD_TRUNCATION_MARKER}`;
return `${utf8HeadExcerpt(line, budget).text}${RECENT_RECORD_TRUNCATION_MARKER}`;
};
/** Bound parser memory when a capped MI record contains extreme nesting. */
+3 -3
View File
@@ -21,7 +21,7 @@ import {
prepareOwnedCommand,
} from "../process/OwnedCommand.js";
import type { ProviderProcessSnapshot } from "../process/ProviderProcess.js";
import { boundedUtf8Text } from "../process/boundedUtf8Text.js";
import { utf8HeadExcerpt } from "../domain/utf8HeadExcerpt.js";
import { constants, accessSync, statSync } from "node:fs";
import { posix, win32 } from "node:path";
import { err, ok, type Result } from "../domain/result.js";
@@ -87,11 +87,11 @@ const JAVA_DIAGNOSTIC_STREAM_BYTES = 8192;
const capturedProbeOutput = (
observed: ProviderProcessSnapshot,
): AnalysisCapturedOutput => {
const stdout = boundedUtf8Text(
const stdout = utf8HeadExcerpt(
observed.stdout.text,
JAVA_DIAGNOSTIC_STREAM_BYTES,
);
const stderr = boundedUtf8Text(
const stderr = utf8HeadExcerpt(
observed.stderr.text,
JAVA_DIAGNOSTIC_STREAM_BYTES,
);
+2 -2
View File
@@ -41,7 +41,7 @@ import {
execFileOutputFailure,
execFileFailureCapturedOutput,
} from "../process/ExecFileOutput.js";
import { boundedUtf8Text } from "../process/boundedUtf8Text.js";
import { utf8HeadExcerpt } from "../domain/utf8HeadExcerpt.js";
import { redactCapturedTransportCredential } from "../process/ProviderDiagnosticRedaction.js";
import {
cleanupOwnedProcessGroup,
@@ -522,7 +522,7 @@ export class GhidraHeadlessLauncher implements GhidraLauncher {
session.token,
"[REDACTED]",
);
const bounded = boundedUtf8Text(
const bounded = utf8HeadExcerpt(
redacted,
GHIDRA_LAUNCH_COMMAND_OUTPUT_BYTES,
);
+15 -15
View File
@@ -5,7 +5,7 @@ import {
} from "../domain/analysisErrorBase.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { DarwinProcessOwnershipInspectionError } from "../process/DarwinProcessRunTokenReader.js";
import { boundedUtf8Text } from "../process/boundedUtf8Text.js";
import { utf8HeadExcerpt } from "../domain/utf8HeadExcerpt.js";
import type { GhidraDiagnosticsScope } from "./GhidraDiagnostics.js";
import { isGhidraByteSearchCursorMismatch } from "./GhidraByteSearchCursorFailure.js";
import { ghidraFileOffsetOutcome } from "./GhidraFileOffsetFailure.js";
@@ -264,7 +264,7 @@ const describeCause = (
return describeErrorCause(cause, redact, depth);
}
if (typeof cause === "string") {
const message = boundedUtf8Text(redact(cause), MAX_CAUSE_MESSAGE_BYTES);
const message = utf8HeadExcerpt(redact(cause), MAX_CAUSE_MESSAGE_BYTES);
return {
type: "string",
message: message.text,
@@ -283,11 +283,11 @@ const describeErrorCause = (
redact: (value: string, incomplete?: boolean) => string,
depth: number,
): JsonValue => {
const message = boundedUtf8Text(
const message = utf8HeadExcerpt(
redact(cause.message),
MAX_CAUSE_MESSAGE_BYTES,
);
const name = boundedUtf8Text(redact(cause.name), MAX_CAUSE_FIELD_BYTES);
const name = utf8HeadExcerpt(redact(cause.name), MAX_CAUSE_FIELD_BYTES);
return {
name: name.text,
...(name.truncated ? { name_truncated: true } : {}),
@@ -311,11 +311,11 @@ const causeCommandOutcome = (
if (!(cause instanceof GhidraLaunchSupportError)) return {};
const outcome = cause.outcome;
if (outcome === undefined) return {};
const stdout = boundedUtf8Text(
const stdout = utf8HeadExcerpt(
redact(outcome.stdout, outcome.outputTruncated !== false),
MAX_COMMAND_OUTPUT_BYTES,
);
const stderr = boundedUtf8Text(
const stderr = utf8HeadExcerpt(
redact(outcome.stderr, outcome.outputTruncated !== false),
MAX_COMMAND_OUTPUT_BYTES,
);
@@ -345,7 +345,7 @@ const causeAdmissionDetail = (
detail[key] = value;
continue;
}
const bounded = boundedUtf8Text(redact(value), MAX_CAUSE_FIELD_BYTES);
const bounded = utf8HeadExcerpt(redact(value), MAX_CAUSE_FIELD_BYTES);
detail[key] = bounded.text;
}
return { detail };
@@ -357,7 +357,7 @@ const causeCode = (
): Record<string, JsonValue> => {
const code = "code" in cause ? cause.code : undefined;
if (typeof code === "string") {
const bounded = boundedUtf8Text(redact(code), MAX_CAUSE_FIELD_BYTES);
const bounded = utf8HeadExcerpt(redact(code), MAX_CAUSE_FIELD_BYTES);
return {
code: bounded.text,
...(bounded.truncated ? { code_truncated: true } : {}),
@@ -373,7 +373,7 @@ const causeSignal = (
): Record<string, JsonValue> => {
const signal = "signal" in cause ? cause.signal : undefined;
if (typeof signal === "string") {
const bounded = boundedUtf8Text(redact(signal), MAX_CAUSE_FIELD_BYTES);
const bounded = utf8HeadExcerpt(redact(signal), MAX_CAUSE_FIELD_BYTES);
return {
signal: bounded.text,
...(bounded.truncated ? { signal_truncated: true } : {}),
@@ -390,7 +390,7 @@ const causeCompilerOutcome = (
if (!(cause instanceof DarwinProcessOwnershipInspectionError)) return {};
const outcome = cause.compilerOutcome;
if (outcome === undefined) return {};
const stderr = boundedUtf8Text(
const stderr = utf8HeadExcerpt(
redact(outcome.stderr),
MAX_COMPILER_STDERR_BYTES,
);
@@ -414,7 +414,7 @@ const boundedCompilerField = (
redact: (value: string, incomplete?: boolean) => string,
): { readonly value: string | number | null; readonly truncated: boolean } => {
if (typeof value !== "string") return { value, truncated: false };
const bounded = boundedUtf8Text(redact(value), MAX_CAUSE_FIELD_BYTES);
const bounded = utf8HeadExcerpt(redact(value), MAX_CAUSE_FIELD_BYTES);
return { value: bounded.text, truncated: bounded.truncated };
};
@@ -440,13 +440,13 @@ const causeLaunchRollback = (
},
};
const result = rollback.result;
const reason = boundedUtf8Text(
const reason = utf8HeadExcerpt(
redact(result.reason),
MAX_CAUSE_MESSAGE_BYTES,
);
const diagnostic = (value: string | undefined): Record<string, JsonValue> => {
if (value === undefined) return {};
const bounded = boundedUtf8Text(redact(value), MAX_CAUSE_MESSAGE_BYTES);
const bounded = utf8HeadExcerpt(redact(value), MAX_CAUSE_MESSAGE_BYTES);
return {
diagnostic: bounded.text,
...(bounded.truncated ? { diagnostic_truncated: true } : {}),
@@ -533,11 +533,11 @@ const causeLaunchOutcome = (
if (cause.cause instanceof GhidraLaunchSupportError) return launchSupport;
const output = cause.capturedOutput;
if (output === undefined) return launchSupport;
const stdout = boundedUtf8Text(
const stdout = utf8HeadExcerpt(
redact(output.stdout),
GHIDRA_LAUNCH_COMMAND_OUTPUT_BYTES,
);
const stderr = boundedUtf8Text(
const stderr = utf8HeadExcerpt(
redact(output.stderr),
GHIDRA_LAUNCH_COMMAND_OUTPUT_BYTES,
);
+3 -3
View File
@@ -17,7 +17,7 @@ import type {
ProviderProcessSupervisor,
SpawnedOwnedProviderProcess,
} from "../process/ProviderProcess.js";
import { boundedUtf8Text } from "../process/boundedUtf8Text.js";
import { utf8HeadExcerpt } from "../domain/utf8HeadExcerpt.js";
import { DEFAULT_NATIVE_COMMAND_TIMEOUT_MS } from "../domain/native/nativeDeadlines.js";
import {
selectArtifactCommandPath,
@@ -482,11 +482,11 @@ export const failureCapture = (
capture: ProcessCapture,
truncated: boolean,
): NativeCommandFailureCapture => {
const stdout = boundedUtf8Text(
const stdout = utf8HeadExcerpt(
capture.stdout,
NATIVE_FAILURE_DIAGNOSTIC_BYTES,
);
const stderr = boundedUtf8Text(
const stderr = utf8HeadExcerpt(
capture.stderr,
NATIVE_FAILURE_DIAGNOSTIC_BYTES,
);
+4 -4
View File
@@ -25,7 +25,7 @@ import type {
} from "../domain/analysisErrorBase.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import { safeParseJson } from "../domain/safeJson.js";
import { boundedUtf8Text } from "./boundedUtf8Text.js";
import { utf8HeadExcerpt } from "../domain/utf8HeadExcerpt.js";
import { createDarwinHelperCache } from "./DarwinHelperCache.js";
import { snapshotEnvironment } from "./snapshotEnvironment.js";
import {
@@ -266,8 +266,8 @@ const compilerFailureFacts = (
| undefined => {
const failure = execFileOutputFailure(cause);
if (failure === undefined) return undefined;
const stderr = boundedUtf8Text(failure.stderr, MAX_COMPILER_OUTPUT_BYTES);
const stdout = boundedUtf8Text(failure.stdout, MAX_COMPILER_OUTPUT_BYTES);
const stderr = utf8HeadExcerpt(failure.stderr, MAX_COMPILER_OUTPUT_BYTES);
const stdout = utf8HeadExcerpt(failure.stdout, MAX_COMPILER_OUTPUT_BYTES);
const outcome =
failure.code === "ETIMEDOUT"
? "timed_out"
@@ -336,7 +336,7 @@ const compilerInspectionError = (
code === "ENOENT"
? `macOS process ownership inspection requires the Apple Swift compiler via ${launcher}`
: `macOS process ownership helper compilation failed via ${launcher}`;
const stderr = boundedUtf8Text(outcome?.stderr ?? "", STDERR_EXCERPT_LIMIT);
const stderr = utf8HeadExcerpt(outcome?.stderr ?? "", STDERR_EXCERPT_LIMIT);
const diagnostics = stderr.text.trim();
const truncated =
stderr.truncated ||
-16
View File
@@ -1,16 +0,0 @@
/** Keep a UTF-8 string within a byte budget without cutting a code point. */
export const boundedUtf8Text = (
value: string,
maximumBytes: number,
): { readonly text: string; readonly truncated: boolean } => {
let bytes = 0;
let end = 0;
for (const codePoint of value) {
const codePointBytes = Buffer.byteLength(codePoint, "utf8");
if (bytes + codePointBytes > maximumBytes)
return { text: value.slice(0, end), truncated: true };
bytes += codePointBytes;
end += codePoint.length;
}
return { text: value, truncated: false };
};
+7 -12
View File
@@ -62,6 +62,7 @@ import {
} from "../process/ownedDebuggerSession.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
import { ABORTED, waitForAbortable } from "../process/AbortablePromise.js";
import { utf8HeadExcerpt } from "../domain/utf8HeadExcerpt.js";
const CLEANUP_COMMAND_TIMEOUT_MS = 1_000;
const STARTUP_TIMEOUT_MS = 30_000;
@@ -1287,7 +1288,7 @@ export class RizinDebugSessionManager {
const protocolOutput = [...session.history, partialFrame]
.filter((frame) => frame.length > 0)
.join("\n");
const stdout = truncateUtf8(protocolOutput, MAX_DIAGNOSTIC_BYTES);
const stdout = utf8HeadExcerpt(protocolOutput, MAX_DIAGNOSTIC_BYTES).text;
return {
stdout,
stderr: output.stderr.text,
@@ -1317,7 +1318,10 @@ export class RizinDebugSessionManager {
synchronization.resolve();
return;
}
const historyPrefix = truncateUtf8(frame.text, MAX_RECENT_FRAME_BYTES);
const historyPrefix = utf8HeadExcerpt(
frame.text,
MAX_RECENT_FRAME_BYTES,
).text;
const historyTruncated =
historyPrefix.length < frame.text.length || frame.truncated;
if (historyTruncated) session.historyTruncated = true;
@@ -1906,7 +1910,7 @@ const waitForCommandFrames = async (
const bytes = Buffer.byteLength(text, "utf8");
if (bytes > remaining) {
noteLimit("command_output_bytes");
const retained = truncateUtf8(text, remaining);
const retained = utf8HeadExcerpt(text, remaining).text;
if (retained.length > 0) output.push(retained);
outputBytes = maxCommandOutputBytes;
} else {
@@ -2012,15 +2016,6 @@ const waitForCommandFrames = async (
}
};
const truncateUtf8 = (value: string, maximumBytes: number): string => {
if (maximumBytes <= 0) return "";
// A UTF-16 code unit needs at most three UTF-8 bytes. Buffer.write stops
// before a partial character; only decode bytes it actually initialized.
const bytes = Buffer.allocUnsafe(Math.min(value.length * 3, maximumBytes));
const written = bytes.write(value, "utf8");
return bytes.toString("utf8", 0, written);
};
const waitForFrame = (
session: RizinDebugSession,
timeoutMs: number,
+3 -3
View File
@@ -6,7 +6,7 @@ import {
artifactFilePrimaryFacts,
bindArtifactFileCleanup,
} from "../artifacts/ArtifactFileOperation.js";
import { boundedUtf8Text } from "../process/boundedUtf8Text.js";
import { utf8HeadExcerpt } from "../domain/utf8HeadExcerpt.js";
import type { ProviderProcessSnapshot } from "../process/ProviderProcess.js";
import {
AnalysisError,
@@ -88,9 +88,9 @@ export const wabtConfigurationRecoveryAction = (cause?: unknown): string => {
};
/** Past the output budget, retained text is a prefix of an oversized stream; keep a bounded excerpt of each. */
const outputLimitExcerpt = (snapshot: ProviderProcessSnapshot) => ({
stdout: boundedUtf8Text(snapshot.stdout.text, OUTPUT_LIMIT_EXCERPT_BYTES)
stdout: utf8HeadExcerpt(snapshot.stdout.text, OUTPUT_LIMIT_EXCERPT_BYTES)
.text,
stderr: boundedUtf8Text(snapshot.stderr.text, OUTPUT_LIMIT_EXCERPT_BYTES)
stderr: utf8HeadExcerpt(snapshot.stderr.text, OUTPUT_LIMIT_EXCERPT_BYTES)
.text,
// The command reports output-limit only after retention was truncated.
truncated: true,