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

shell: Manifest validation upon import #21449

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

mvollmer
Copy link
Member

@mvollmer mvollmer commented Dec 18, 2024

  • unit tests

@mvollmer mvollmer added the blocked Don't land until something else happens first (see task list) label Dec 18, 2024
@mvollmer mvollmer changed the title Shell validation shell: Manifest validation upon import Dec 18, 2024
@mvollmer
Copy link
Member Author

This is quite WIPy still...

@mvollmer mvollmer added no-test For doc/workflow changes, or experiments which don't need a full CI run, and removed blocked Don't land until something else happens first (see task list) labels Dec 18, 2024
This way, it is only validated once and not on every render.
@mvollmer mvollmer removed the no-test For doc/workflow changes, or experiments which don't need a full CI run, label Jan 7, 2025
This make sure that typed code never sees values that don't conform to
the claimed types, no matter what people put into their manifests.
Comment on lines +169 to +171
function validation_error(msg: string): never {
console.error(`JSON validation error for ${validation_path.join("")}: ${msg}`);
throw new ValidationError();
Copy link
Contributor

Choose a reason for hiding this comment

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

These 3 added lines are not executed by any test.

export function import_string(val: JsonValue): string {
if (typeof val == "string")
return val;
validation_error(`Not a string: ${JSON.stringify(val)}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

This added line is not executed by any test.

export function import_number(val: JsonValue): number {
if (typeof val == "number")
return val;
validation_error(`Not a number: ${JSON.stringify(val)}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

This added line is not executed by any test.

Comment on lines +186 to +189
export function import_boolean(val: JsonValue): boolean {
if (typeof val == "boolean")
return val;
validation_error(`Not a boolean: ${JSON.stringify(val)}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

These 4 added lines are not executed by any test.

export function import_json_object(val: JsonValue): JsonObject {
if (!!val && typeof val == "object" && val.length === undefined)
return val as JsonObject;
validation_error(`Not an object: ${JSON.stringify(val)}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

This added line is not executed by any test.

Comment on lines +238 to +240
} catch (e) {
if (!(e instanceof ValidationError))
throw e;
Copy link
Contributor

Choose a reason for hiding this comment

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

These 3 added lines are not executed by any test.

Comment on lines +245 to +246
if (obj[field as string] === undefined) {
validation_error(`Field "${String(field)}" is missing`);
Copy link
Contributor

Choose a reason for hiding this comment

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

These 2 added lines are not executed by any test.


export function validate<T>(path: string, val: JsonValue | undefined, importer: (val: JsonValue) => T, fallback: T): T {
if (val === undefined)
return fallback;
Copy link
Contributor

Choose a reason for hiding this comment

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

This added line is not executed by any test.

Comment on lines +257 to +260
} catch (e) {
if (!(e instanceof ValidationError))
throw e;
return fallback;
Copy link
Contributor

Choose a reason for hiding this comment

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

These 4 added lines are not executed by any test.

@@ -144,7 +142,7 @@ export const LangModal = ({ dialogResult }) => {
<MenuList>
{
(() => {
const locales = manifest.locales || {};
const locales = state.config.manifest.locales || {};
Copy link
Contributor

Choose a reason for hiding this comment

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

This added line is not executed by any test.

@mvollmer mvollmer requested a review from martinpitt January 8, 2025 10:01
@mvollmer mvollmer marked this pull request as ready for review January 8, 2025 10:01
Copy link
Member

@martinpitt martinpitt left a comment

Choose a reason for hiding this comment

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

Thanks @mvollmer ! I like the approach a lot -- explicit, clean, no new dependencies, and pleasant to use.

QUnit.test("import_array", function(assert) {
assert.deepEqual(import_array(["a", "b", "c"], import_string), ["a", "b", "c"], "array of strings");
assert.deepEqual(import_array([1, 2, 3], import_number), [1, 2, 3], "array of numbers");
assert.deepEqual(import_array([1, 2, "c"], import_number), [1, 2], "array of numbers with a string");
Copy link
Member

Choose a reason for hiding this comment

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

IMHO this should also throw, instead of giving you a truncated array -- that just silently hides errors, and may make the rest of the entries invalid as well.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, this is the big philosophical question. Personally, I like the non-strict approach, but I have no problem with making this strict.

My best argument for making this strict is to amplify that something is wrong. If you make a mistake with your search terms, your whole page doesn't get loaded instead of some search terms being omitted.


QUnit.test("import_record", function(assert) {
assert.deepEqual(
import_record({ a: "a", b: "b", c: "c" }, import_string),
Copy link
Member

Choose a reason for hiding this comment

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

JSON parlance is "object", not "record". I suppose you want to give this a different name to point out that all values have to be of the same type. But a "record" (or "struct") is precisely the opposite -- fixed number and names of keys, and different values.

One term that comes to my mind that conveys this meaning is "dictionary" (they usually map one type to a single value type).

Copy link
Member Author

Choose a reason for hiding this comment

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

The "record" term is from the TypeScript utility class Record<Keys, Type>, which is indeed a Map or Dict, not a structure.

Copy link
Member

Choose a reason for hiding this comment

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

Ugh -- this is opposite to pretty much everywhere else https://en.wikipedia.org/wiki/Record_(computer_science) -- shame on you, MS.


import { JsonValue } from 'cockpit';
import {
import_json_object, import_optional, import_mandatory,
Copy link
Member

Choose a reason for hiding this comment

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

get_() would be more consistent with our Python API; "import" sounds a bit exaggerated, but no strong opinion.

Comment on lines +103 to +106
assert.deepEqual(
import_record({ a: 1, b: 2, c: "c" }, import_number),
{ a: 1, b: 2 },
"record of numbers with a string");
Copy link
Member

Choose a reason for hiding this comment

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

Similar to the inconsistent array above, IMHO this should throw and ignore the value in its entirety.

Comment on lines +47 to +51
Validation is generally lenient: If a validation error occurs deep
inside a nested structure, only the affected part of the structure
is omitted. More conretely, if an element of an array is invalid,
this element is omitted from the array. If a optional field of an
object is invalid, it will be omitted. This doesn't happen
Copy link
Member

Choose a reason for hiding this comment

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

This is what I mentioned above: IMHO this isn't safe in general. Think about "bridges": [] in the shell manifest. Silently ignoring a non-string argument from sudo or pkexec is potentially disastrous. It's of course less of an issue for docs or locales, but IMHO it would make more sense to be strict and completely ignore the value -- that will greatly increase the chance of catching bugs in our tests.

(Also, "conretely" typo)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, command lines are a very good example of something that better be rejected in its entirety.

const checks: [JsonValue, Teams][] = [
[
{
MAC: {
Copy link
Member

Choose a reason for hiding this comment

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

Sorry, I just have to ask: What does "MAC" mean in soccer context?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's supposed to be the short form of "ManCity"...

{ name: "Haaland", age: 24 },
{ name: "De Bruyne", age: 33 }
],
},
Copy link
Member

Choose a reason for hiding this comment

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

Can you please add a description comment before each scenario? Like, this one is probably "No stadium (it's optional)" or so

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep.


function validation_error(msg: string): never {
console.error(`JSON validation error for ${validation_path.join("")}: ${msg}`);
throw new ValidationError();
Copy link
Member

Choose a reason for hiding this comment

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

Couldn't hurt to pass on msg into the exception object?

pkg/lib/import-json.ts Show resolved Hide resolved
pkg/shell/machines/machines.js Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants