Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fix-resolved-value-jitless.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@inversifyjs/core": patch
"inversify": patch
---

- Fixed resolved value binding resolution to respect the `jitless` container option, avoiding `Function` constructor usage in CSP-restricted environments.
5 changes: 5 additions & 0 deletions .changeset/tough-moons-leave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@inversifyjs/core": patch
---

- Updated `currySubplan` with missing depth detection on resolve value plans.
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ function curryBuildResolvedValuePlanBindingNode(
bindingConstraintsList: SingleImmutableLinkedList<InternalBindingConstraints>,
): PlanBindingNode => {
const childNode: ResolvedValueBindingNode =
new ResolvedValueBindingNodeImplementation(binding);
new ResolvedValueBindingNodeImplementation(binding, params.jitEnabled);

const subplanParams: SubplanParams = {
autobindOptions: params.autobindOptions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,13 @@ function curryHandlePlanServiceNodeBuildFromResolvedValueElementMetadata(
bindingConstraintsList: SingleImmutableLinkedList<InternalBindingConstraints>,
elementMetadata: ResolvedValueElementMetadata,
): PlanServiceNode => {
if (bindingConstraintsList.length > MAX_PLAN_DEPTH) {
throw new InversifyCoreError(
InversifyCoreErrorKind.planningMaxDepthExceeded,
'Maximum plan depth exceeded. This is likely caused by a circular dependency.',
);
}

const getPlanOptions: GetPlanOptions | undefined =
tryBuildGetPlanOptionsFromResolvedValueElementMetadata(elementMetadata);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { beforeAll, describe, expect, it } from 'vitest';

import { type ResolvedValueBinding } from '../../binding/models/ResolvedValueBinding.js';
import { type ResolutionParams } from '../../resolution/models/ResolutionParams.js';
import { type Resolved } from '../../resolution/models/Resolved.js';
import { type PlanServiceNode } from '../models/PlanServiceNode.js';
import { type ResolvedValueBindingNode } from '../models/ResolvedValueBindingNode.js';
import { buildFourResolvedValueArgumentResolverOnCsp } from './buildFourResolvedValueArgumentResolverOnCsp.js';

class TestFixtures {
public static get node(): ResolvedValueBindingNode<
ResolvedValueBinding<string>
> {
return {
binding: {
factory: (
value0: unknown,
value1: unknown,
value2: unknown,
value3: unknown,
): string =>
`factory:${String(value0)}:${String(value1)}:${String(value2)}:${String(value3)}`,
metadata: {
arguments: [Symbol(), Symbol(), Symbol(), Symbol()],
},
},
params: [],
} as unknown as ResolvedValueBindingNode<ResolvedValueBinding<string>>;
}

public static get params(): ResolutionParams {
return Symbol() as unknown as ResolutionParams;
}
}

function buildParamsFixture(
resolve0: () => unknown,
resolve1: () => unknown,
resolve2: () => unknown,
resolve3: () => unknown,
): PlanServiceNode[] {
return [
{
resolve: resolve0,
} as Partial<PlanServiceNode> as PlanServiceNode,
{
resolve: resolve1,
} as Partial<PlanServiceNode> as PlanServiceNode,
{
resolve: resolve2,
} as Partial<PlanServiceNode> as PlanServiceNode,
{
resolve: resolve3,
} as Partial<PlanServiceNode> as PlanServiceNode,
];
}

describe(buildFourResolvedValueArgumentResolverOnCsp, () => {
describe('having a resolved value binding node', () => {
describe('when called, and node.params is populated after the resolver is built', () => {
let result: unknown;

beforeAll(() => {
const nodeFixture: ResolvedValueBindingNode<
ResolvedValueBinding<string>
> = TestFixtures.node;
const resolveNode: (params: ResolutionParams) => Resolved<string> =
buildFourResolvedValueArgumentResolverOnCsp(
nodeFixture,
(
_params: ResolutionParams,
resolvedValue: Resolved<string>,
): Resolved<string> => resolvedValue,
);

nodeFixture.params.push(
...buildParamsFixture(
(): string => 'value-0',
(): string => 'value-1',
(): string => 'value-2',
(): string => 'value-3',
),
);

result = resolveNode(TestFixtures.params);
});

it('should call the factory with the resolved arguments', () => {
expect(result).toBe('factory:value-0:value-1:value-2:value-3');
});
});

describe.each<
[string, () => unknown, () => unknown, () => unknown, () => unknown]
>([
[
'all arguments resolve asynchronously',
async (): Promise<string> => 'value-0',
async (): Promise<string> => 'value-1',
async (): Promise<string> => 'value-2',
async (): Promise<string> => 'value-3',
],
[
'the first argument resolves asynchronously',
async (): Promise<string> => 'value-0',
(): string => 'value-1',
(): string => 'value-2',
(): string => 'value-3',
],
[
'the second argument resolves asynchronously',
(): string => 'value-0',
async (): Promise<string> => 'value-1',
(): string => 'value-2',
(): string => 'value-3',
],
[
'the third argument resolves asynchronously',
(): string => 'value-0',
(): string => 'value-1',
async (): Promise<string> => 'value-2',
(): string => 'value-3',
],
[
'the fourth argument resolves asynchronously',
(): string => 'value-0',
(): string => 'value-1',
(): string => 'value-2',
async (): Promise<string> => 'value-3',
],
])(
'when called, and %s',
(
_: string,
resolve0: () => unknown,
resolve1: () => unknown,
resolve2: () => unknown,
resolve3: () => unknown,
) => {
let result: unknown;

beforeAll(async () => {
const nodeFixture: ResolvedValueBindingNode<
ResolvedValueBinding<string>
> = TestFixtures.node;
const resolveNode: (params: ResolutionParams) => Resolved<string> =
buildFourResolvedValueArgumentResolverOnCsp(
nodeFixture,
(
_params: ResolutionParams,
resolvedValue: Resolved<string>,
): Resolved<string> => resolvedValue,
);

nodeFixture.params.push(
...buildParamsFixture(resolve0, resolve1, resolve2, resolve3),
);

result = await resolveNode(TestFixtures.params);
});

it('should call the factory with the resolved arguments', () => {
expect(result).toBe('factory:value-0:value-1:value-2:value-3');
});
},
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { type ResolvedValueBinding } from '../../binding/models/ResolvedValueBinding.js';
import { type ResolutionParams } from '../../resolution/models/ResolutionParams.js';
import { type Resolved } from '../../resolution/models/Resolved.js';
import { type PlanServiceNode } from '../models/PlanServiceNode.js';
import { type ResolvedValueBindingNode } from '../models/ResolvedValueBindingNode.js';
import { resolveFour } from './resolveFour.js';

/**
* Same rationale as buildZeroConstructorArgumentsResolverOnCsp, but for
* four-argument resolved value bindings. Equivalent to
* `buildResolvedValueArgumentsResolver` with `resolveFour`, but implemented
* with a plain closure instead of the `Function` constructor, so it works in
* environments enforcing a strict Content Security Policy (no
* `unsafe-eval`).
*/
export function buildFourResolvedValueArgumentResolverOnCsp<TActivated>(
node: ResolvedValueBindingNode<ResolvedValueBinding<TActivated>>,
resolveActivations: (
params: ResolutionParams,
resolvedValue: Resolved<TActivated>,
) => Resolved<TActivated>,
): (params: ResolutionParams) => Resolved<TActivated> {
return function resolveNode(params: ResolutionParams): Resolved<TActivated> {
const resolvedValue0: unknown = (node.params[0] as PlanServiceNode).resolve(
params,
);
const resolvedValue1: unknown = (node.params[1] as PlanServiceNode).resolve(
params,
);
const resolvedValue2: unknown = (node.params[2] as PlanServiceNode).resolve(
params,
);
const resolvedValue3: unknown = (node.params[3] as PlanServiceNode).resolve(
params,
);

return resolveFour(
resolvedValue0,
resolvedValue1,
resolvedValue2,
resolvedValue3,
(
resolvedValue0: unknown,
resolvedValue1: unknown,
resolvedValue2: unknown,
resolvedValue3: unknown,
): Resolved<TActivated> =>
resolveActivations(
params,
node.binding.factory(
resolvedValue0,
resolvedValue1,
resolvedValue2,
resolvedValue3,
),
),
);
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { type ServiceIdentifier } from '@inversifyjs/common';

import { type ResolvedValueBinding } from '../../binding/models/ResolvedValueBinding.js';
import { resolveResolvedValueBindingNode } from '../../resolution/actions/resolveResolvedValueBindingNode.js';
import { resolveScopedWithNoActivations } from '../../resolution/actions/resolveScopedWithNoActivations.js';
import { resolveServiceActivations } from '../../resolution/actions/resolveServiceActivations.js';
import { type ResolutionParams } from '../../resolution/models/ResolutionParams.js';
import { type Resolved } from '../../resolution/models/Resolved.js';
import { type ResolvedValueBindingNode } from '../models/ResolvedValueBindingNode.js';
import { buildOneResolvedValueArgumentResolver } from './buildOneResolvedValueArgumentResolver.js';
import { buildResolvedValueArgumentsResolver } from './buildResolvedValueArgumentsResolver.js';
import { buildZeroResolvedValueArgumentsResolver } from './buildZeroResolvedValueArgumentsResolver.js';
import { resolveFour } from './resolveFour.js';
import { resolveThree } from './resolveThree.js';
import { resolveTwo } from './resolveTwo.js';

const ZERO_PARAMS: number = 0;
const ONE_PARAM: number = 1;
const TWO_PARAMS: number = 2;
const THREE_PARAMS: number = 3;
const FOUR_PARAMS: number = 4;

/**
* Builds a resolver for resolved value binding nodes with no binding
* activation.
*
* The resolution logic is specialized by argument arity to minimize function
* call dispatch overhead on the hottest resolution path.
*/
export function buildNoActivationsResolvedValueBindingNodeResolver<TActivated>(
node: ResolvedValueBindingNode<ResolvedValueBinding<TActivated>>,
): (params: ResolutionParams) => Resolved<TActivated> {
const serviceIdentifier: ServiceIdentifier<TActivated> =
node.binding.serviceIdentifier;

const resolveActivations: (
params: ResolutionParams,
value: Resolved<TActivated>,
) => Resolved<TActivated> = resolveServiceActivations(serviceIdentifier);

let resolveNode: (params: ResolutionParams) => Resolved<TActivated>;

switch (node.binding.metadata.arguments.length) {
case ZERO_PARAMS:
resolveNode = buildZeroResolvedValueArgumentsResolver(
node,
resolveActivations,
);
break;
case ONE_PARAM:
resolveNode = buildOneResolvedValueArgumentResolver(
node,
resolveActivations,
);
break;
case TWO_PARAMS:
resolveNode = buildResolvedValueArgumentsResolver(
node,
resolveActivations,
resolveTwo,
);
break;
case THREE_PARAMS:
resolveNode = buildResolvedValueArgumentsResolver(
node,
resolveActivations,
resolveThree,
);
break;
case FOUR_PARAMS:
resolveNode = buildResolvedValueArgumentsResolver(
node,
resolveActivations,
resolveFour,
);
break;
default:
resolveNode = (params: ResolutionParams): Resolved<TActivated> =>
resolveActivations(
params,
resolveResolvedValueBindingNode(params, node),
);
}

return resolveScopedWithNoActivations(node.binding, resolveNode);
}
Loading
Loading