Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(builder): Add packageref validation for name and variant size #1185

Merged
merged 38 commits into from
Jul 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d51dc32
Fix for name and variant length overflow
FuzzB0t Jul 2, 2024
8bd0566
Fix for name and variant length overflow
FuzzB0t Jul 2, 2024
882c98a
fix lint issues
FuzzB0t Jul 2, 2024
14bbb22
Merge branch 'main' into packageref-validation
FuzzB0t Jul 2, 2024
e75a9b1
Refactor validation logic
FuzzB0t Jul 2, 2024
515f683
Refactor validations
FuzzB0t Jul 2, 2024
d965ab0
lint fixes
FuzzB0t Jul 2, 2024
30842ef
Refactor validations
FuzzB0t Jul 2, 2024
08a98fa
misc
FuzzB0t Jul 2, 2024
e89ce24
Regen package-lock
FuzzB0t Jul 2, 2024
cb7d493
Refactoring target and source validations
FuzzB0t Jul 3, 2024
7c983a3
lint fixes
FuzzB0t Jul 3, 2024
3bdf661
refactor validations
FuzzB0t Jul 3, 2024
268ceff
lint fixes
FuzzB0t Jul 3, 2024
39fb98e
Add tests for name validation
FuzzB0t Jul 4, 2024
f559009
lint fixes
FuzzB0t Jul 4, 2024
848e54e
Adding tests for version validation
FuzzB0t Jul 4, 2024
3db7fd3
Refactor chainDefinition name and version validation
FuzzB0t Jul 5, 2024
a9b37fd
lint fixes
FuzzB0t Jul 5, 2024
c287256
fix def version validation
FuzzB0t Jul 5, 2024
b944b26
Refactor package version validation
FuzzB0t Jul 5, 2024
099b564
lint fixes
FuzzB0t Jul 5, 2024
76c73d0
lint fixes
FuzzB0t Jul 5, 2024
df0884f
Remove use of blob for length validation
FuzzB0t Jul 5, 2024
c2bf838
Merge branch 'main' into packageref-validation
FuzzB0t Jul 5, 2024
f1f5ecf
Merge branch 'main' into packageref-validation
FuzzB0t Jul 6, 2024
7b48a06
Adding length validation to package ref isValid function
FuzzB0t Jul 7, 2024
4fb44c9
Merge branch 'packageref-validation' of github.com:usecannon/cannon i…
FuzzB0t Jul 7, 2024
652d215
Remove package import from schemas
FuzzB0t Jul 7, 2024
65685b9
Remove package import from schemas
FuzzB0t Jul 7, 2024
1018bca
Merge branch 'main' into packageref-validation
FuzzB0t Jul 9, 2024
b39da33
Merge branch 'main' into packageref-validation
FuzzB0t Jul 10, 2024
57dcaf4
Merge branch 'main' into packageref-validation
saeta-eth Jul 11, 2024
dd6b584
Merge branch 'main' into packageref-validation
FuzzB0t Jul 11, 2024
fb486ac
Adding requested changes
FuzzB0t Jul 11, 2024
4968ccd
Merge branch 'packageref-validation' of github.com:usecannon/cannon i…
FuzzB0t Jul 11, 2024
da4f6a7
fix version validation
FuzzB0t Jul 11, 2024
978931a
fix lint issues
FuzzB0t Jul 11, 2024
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
2 changes: 0 additions & 2 deletions examples/sample-foundry-project/cannonfile.consumer.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,9 @@ args = ["<%= contracts.cloned.address %>", "<%= formatBytes32String(settings.cha

var.NewCloneGreeting.event = "NewGreetingAdded"
var.NewCloneGreeting.arg = 0
var.NewCloneGreeting.artifact = "Greeter"

var.OldCloneGreeting.event = "OldGreetingRemoved"
var.OldCloneGreeting.arg = 0
var.OldCloneGreeting.artifact = "Greeter"

# test to parse through previous emitted event values
[invoke.set_new_greeting_for_next_clones]
Expand Down
7 changes: 4 additions & 3 deletions examples/sample-hardhat-project/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13,039 changes: 3,991 additions & 9,048 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions packages/builder/src/definition.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ describe('ChainDefinition', () => {

expect(() => new ChainDefinition(rawDef)).toThrow('Unrecognized action type invalid at [invalid.saturday]');
});

it('throws an error when trying to use a definition with name over 32 bytes', () => {
const rawDef = {
name: 'package-name-longer-than-32bytes1337',
version: '1.0.0',
};

expect(() => new ChainDefinition(rawDef)).toThrow('Package name exceeds 32 bytes');
});

it('throws an error when trying to use a definition with version over 32 bytes', () => {
const rawDef = {
name: 'package',
version: 'package-version-longer-than-32bytes1337',
};

expect(() => new ChainDefinition(rawDef)).toThrow('Package version exceeds 32 bytes');
});
});

describe('validatePackageName()', () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/builder/src/error/zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ export const customErrorMap: z.ZodErrorMap = (error, ctx) => {
}
case z.ZodIssueCode.too_big:
return {
message: `Expected ${error.path[0]} to equal less than ${error.maximum} but got ${ctx.data}`,
message: `Expected ${error.path[0]} to equal less than ${error.maximum} but got ${ctx.data.length}`,
};
case z.ZodIssueCode.too_small:
return {
message: `Expected ${error.path[0]} to equal more than ${error.minimum} but got ${ctx.data}`,
message: `Expected ${error.path[0]} to equal more than ${error.minimum} but got ${ctx.data.length}`,
};
case z.ZodIssueCode.invalid_enum_value:
return {
Expand Down
22 changes: 19 additions & 3 deletions packages/builder/src/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export interface PackagePublishCall {
export class PackageReference {
static DEFAULT_TAG = 'latest';
static DEFAULT_PRESET = 'main';
static PACKAGE_REGEX = /^(?<name>@?[a-z0-9][A-Za-z0-9-]{1,30}[a-z0-9])(?::(?<version>[^@]+))?(@(?<preset>[^\s]+))?$/;
static PACKAGE_REGEX = /^(?<name>@?[a-z0-9][A-Za-z0-9-]{1,}[a-z0-9])(?::(?<version>[^@]+))?(@(?<preset>[^\s]+))?$/;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PackageReference.isValid fn now only uses the regex to validate, not the constructor, so it would not return false when given a too long package name.

Copy link
Contributor Author

@FuzzB0t FuzzB0t Jul 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah but we do want it to be more specific as to why the parse function fails, and .match fails when the length regex is set due to the name being too long, so I removed the length limiter on the regex and on parse I added the same errors so that it specifies when the package name is too long if somehow a package makes it through the schema validation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree on the parse functionality with prettier errors, but the PackageReference.isValid should return false when it is too long, and with this change it doesn't. Maybe instead of validating with the regex inside the isValid you can use .parse with a try catch.


/**
* Anything before the colon or an @ (if no version is present) is the package name.
Expand Down Expand Up @@ -76,20 +76,36 @@ export class PackageReference {

if (!match || !match.groups?.name) {
throw new Error(
`Invalid package name "${ref}". Should be of the format <package-name>:<version> or <package-name>:<version>@<preset>`
`Invalid package reference "${ref}". Should be of the format <package-name>:<version> or <package-name>:<version>@<preset>`
);
}

const res: PartialRefValues = { name: match.groups.name };

const nameSize = res.name.length;
if (nameSize > 32) {
throw new Error(`Package reference "${ref}" is too long. Package name exceeds 32 bytes`);
}

if (match.groups.version) res.version = match.groups.version;

const versionSize = res.name.length;
if (versionSize > 32) {
throw new Error(`Package reference "${ref}" is too long. Package version exceeds 32 bytes`);
}

if (match.groups.preset) res.preset = match.groups.preset;

return res;
}

static isValid(ref: string) {
return !!PackageReference.PACKAGE_REGEX.test(ref);
try {
PackageReference.parse(ref);
return true;
} catch (err) {
return false;
}
}

static from(name: string, version?: string, preset?: string) {
Expand Down
100 changes: 96 additions & 4 deletions packages/builder/src/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const argtype: z.ZodLazy<any> = z.lazy(() =>
// <%= string interpolation %>, step.names or property.names, packages:versions
const interpolatedRegex = RegExp(/^<%=\s\w+.+[\w()[\]-]+\s%>$/, 'i');
const stepRegex = RegExp(/^[\w-]+\.[.\w-]+$/, 'i');
const packageRegex = RegExp(/^(?<name>@?[a-z0-9][a-z0-9-]{1,29}[a-z0-9])(?::(?<version>[^@]+))?(@(?<preset>[^\s]+))?$/, 'i');
const packageRegex = RegExp(/^(?<name>@?[a-z0-9][a-z0-9-]{1,}[a-z0-9])(?::(?<version>[^@]+))?(@(?<preset>[^\s]+))?$/, 'i');
const jsonAbiPathRegex = RegExp(/^(?!.*\.d?$).*\.json?$/, 'i');

// This regex matches artifact names which are just capitalized words like solidity contract names
Expand Down Expand Up @@ -209,6 +209,34 @@ export const pullSchema = z
message: `Source value: ${val} must match package formats: "package:version" or "package:version@preset" or operation name "import.Contract" or be an interpolated value`,
})
)
.refine(
(val) => {
const match = val.match(packageRegex);

if (match) {
const nameSize = match!.groups!.name.length;

return nameSize <= 32;
} else {
return true;
}
},
(val) => ({ message: `Package reference "${val}" is too long. Package name exceeds 32 bytes` })
)
.refine(
(val) => {
const match = val.match(packageRegex);

if (match && match!.groups!.version) {
const versionSize = match!.groups!.version.length;

return versionSize <= 32;
} else {
return true;
}
},
(val) => ({ message: `Package reference "${val}" is too long. Package version exceeds 32 bytes` })
)
.describe('Source of the cannonfile package to import from. Can be a cannonfile operation name or package name'),
})
.merge(
Expand Down Expand Up @@ -502,6 +530,33 @@ export const cloneSchema = z
message: `Source value: ${val} must match package formats: "package:version" or "package:version@preset" or be an interpolated value`,
})
)
.refine(
(val) => {
const match = val.match(packageRegex);
if (match) {
const nameSize = match!.groups!.name.length;

return nameSize <= 32;
} else {
return true;
}
},
(val) => ({ message: `Package reference "${val}" is too long. Package name exceeds 32 bytes` })
)
.refine(
(val) => {
const match = val.match(packageRegex);

if (match && match!.groups!.version) {
const versionSize = match!.groups!.version.length;

return versionSize <= 32;
} else {
return true;
}
},
(val) => ({ message: `Package reference "${val}" is too long. Package version exceeds 32 bytes` })
)
.describe('Name of the package to provision'),
})
.merge(
Expand Down Expand Up @@ -536,7 +591,34 @@ export const cloneSchema = z
message: `Target value: ${val} must match package formats: "package:version" or "package:version@preset" or be an interpolated value`,
})
)
.describe('Name of the package to provision'),
.refine(
(val) => {
const match = val.match(packageRegex);
if (match) {
const nameSize = match!.groups!.name.length;

return nameSize <= 32;
} else {
return true;
}
},
(val) => ({ message: `Package reference "${val}" is too long. Package name exceeds 32 bytes` })
)
.refine(
(val) => {
const match = val.match(packageRegex);

if (match && match!.groups!.version) {
const versionSize = match!.groups!.version.length;

return versionSize <= 32;
} else {
return true;
}
},
(val) => ({ message: `Package reference "${val}" is too long. Package version exceeds 32 bytes` })
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These functions should not be repeated, maybe they can be refactored as a single new fn in PackageReference that's called validate and returns undefined in case the given ref is correct, or an error string in case it found one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then, that PackageReference.validate fn can be used by PackageReference.isValid, PackageReference.constructor and also the schema validators

Copy link
Contributor Author

@FuzzB0t FuzzB0t Jul 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's an issue when importing from package.ts through the schema file, which causes the action to register incorrectly, which is why I omitted using it in the schema section alltogether.

Copy link
Contributor Author

@FuzzB0t FuzzB0t Jul 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However inside the schema file I could refactor them into a single function that takes a value and checks the length to be less or equal to 32 bytes, so that we avoid repetition.

)
.describe('Name of the package to clone'),
/**
* (DEPRECATED) use `target` instead. Set the new preset to use for this package.
* Default - "main"
Expand Down Expand Up @@ -654,7 +736,12 @@ export const chainDefinitionSchema = z
name: z
.string()
.min(3)
.max(31)
.refine(
(val) => {
return new Blob([val]).size <= 32;
},
(val) => ({ message: `Package name "${val}" is too long. Package name exceeds 32 bytes` })
)
.refine((val) => !!val.match(RegExp(/[a-zA-Z0-9-]+/, 'gm')), {
message: 'Name cannot contain any special characters',
})
Expand All @@ -664,7 +751,12 @@ export const chainDefinitionSchema = z
*/
version: z
.string()
.max(31)
.refine(
(val) => {
return new Blob([val]).size <= 32;
},
(val) => ({ message: `Package version "${val}" is too long. Package version exceeds 32 bytes` })
)
.refine((val) => !!val.match(RegExp(/[\w.]+/, 'gm')), {
message: 'Version cannot contain any special characters',
})
Expand Down
126 changes: 126 additions & 0 deletions packages/builder/src/steps/clone.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,132 @@ describe('steps/clone.ts', () => {
).rejects.toThrowError('deployment not found');
});

it('throws if source name is longer than 32 bytes', async () => {
await expect(() =>
action.exec(
fakeRuntime,
fakeCtx,
{ source: 'package-name-longer-than-32bytes1337:1.0.0' },
{ name: 'package', version: '1.0.0', currentLabel: 'clone.whatever' }
)
).rejects.toThrowError('Package name exceeds 32 bytes');
});

it('throws if source version is longer than 32 bytes', async () => {
jest.mocked(fakeRuntime.readDeploy).mockResolvedValue({
generator: 'cannon test',
timestamp: 1234,
state: {
'deploy.Woot': {
version: BUILD_VERSION,
hash: 'arst',
artifacts: {
contracts: {
Woot: {
address: '0xfoobar',
abi: [],
deployTxnHash: '0x',
contractName: 'Woot',
sourceName: 'Woot.sol',
deployedOn: 'deploy.Woot',
gasCost: '0',
gasUsed: 0,
},
},
},
},
},
options: {},
def: {
name: 'package',
version: '1.0.0',
var: {
main: {
sophisticated: 'fast',
},
},
clone: {
source: { source: 'package:package-version-longer-than-32bytes1337' },
target: { target: 'package:1.0.0' },
},
} as any,
meta: {},
miscUrl: 'https://something.com',
});

await expect(() =>
action.exec(
fakeRuntime,
fakeCtx,
{ source: 'package:package-version-longer-than-32bytes1337' },
{ name: 'package', version: '1.0.0', currentLabel: 'clone.whatever' }
)
).rejects.toThrowError('Package version exceeds 32 bytes');
});

it('throws if target name is longer than 32 bytes', async () => {
await expect(() =>
action.exec(
fakeRuntime,
fakeCtx,
{ source: 'package:1.0.0', target: 'package-name-longer-than-32bytes1337:1.0.0' },
{ name: 'package', version: '1.0.0', currentLabel: 'clone.whatever' }
)
).rejects.toThrowError('Package name exceeds 32 bytes');
});

it('throws if target version is longer than 32 bytes', async () => {
jest.mocked(fakeRuntime.readDeploy).mockResolvedValue({
generator: 'cannon test',
timestamp: 1234,
state: {
'deploy.Woot': {
version: BUILD_VERSION,
hash: 'arst',
artifacts: {
contracts: {
Woot: {
address: '0xfoobar',
abi: [],
deployTxnHash: '0x',
contractName: 'Woot',
sourceName: 'Woot.sol',
deployedOn: 'deploy.Woot',
gasCost: '0',
gasUsed: 0,
},
},
},
},
},
options: {},
def: {
name: 'package',
version: '1.0.0',
var: {
main: {
sophisticated: 'fast',
},
},
clone: {
source: { source: 'package:package-version-longer-than-32bytes1337' },
target: { target: 'package:package-version-longer-than-32bytes1337' },
},
} as any,
meta: {},
miscUrl: 'https://something.com',
});

await expect(() =>
action.exec(
fakeRuntime,
fakeCtx,
{ source: 'package:1.0.0', target: 'package:package-version-longer-than-32bytes1337' },
{ name: 'package', version: '1.0.0', currentLabel: 'clone.whatever' }
)
).rejects.toThrowError('Package version exceeds 32 bytes');
});

it('returns partial deployment if runtime becomes cancelled', async () => {
await registry.publish(['hello:1.0.0@main'], 1234, 'https://something.com', '');

Expand Down
Loading
Loading