diff --git a/CHANGELOG.md b/CHANGELOG.md index c7957f5..811c52b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- `claro package doctor` now rejects duplicate package entries in `claro.lock` instead of allowing ambiguous lockfile data. - `claro package doctor` now rejects listed package manifests whose `version:` only starts with the supported local package version, such as `version: 10`. - `claro package doctor` now rejects listed package manifests whose `source:` only starts with the supported local source, such as `source: local-extra`. - `claro package doctor` now rejects project files whose `manifest-version:` only starts with the supported value, such as `manifest-version: 10`. diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index 314623f..4b34804 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -107,6 +107,7 @@ Ready now: - `claro package doctor` requires the complete supported `version: 1` value in each local package manifest, so a prefix such as `version: 10` is rejected - `claro package doctor` requires the complete supported `source: local` value in each local package manifest, so a prefix such as `source: local-extra` is rejected - package project entries must be unique; `claro package doctor`, `list`, `add`, `init`, and `lock` reject duplicate names instead of generating ambiguous package or lockfile data +- lockfile package entries must also be unique; `claro package doctor` reports duplicate lock entries instead of accepting ambiguous package/checksum data Still needed: - install from local path diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index aa754dc..3c1cb74 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -33,7 +33,7 @@ See `CURRENT_STATUS.md` for the detailed feature matrix. 2. Keep examples aligned with the modern simple syntax (`END`, `DO`, short `SET`, short `ASK`) while documenting older compatibility forms separately. 3. Expand validation around typecheck diagnostics and package/networking safety. Current Forgejo/Gitea CI runs the documented release gates (`claro validate`, typecheck diagnostics validation, version convention validation, package security validation, and CI workflow coverage validation). Current package safety validation covers safe project creation, rejecting unsafe project names during `claro new`, safe manifest/lockfile creation, rejecting unsafe package names during `package add`, detecting unsafe package names already present in `claro.project` during `package doctor`, refusing to write lockfile data for unsafe package names during `package lock`, making `package list` fail instead of displaying unsafe package entries as normal package names, making `package init` fail instead of reporting the project ready when unsafe package names are already present, making `package add` fail before changing files when unsafe package names are already present, making `package remove` fail instead of reporting success when unsafe package names remain during lockfile refresh, allowing `package remove` to remove an exact unsafe package entry so learners can repair a bad `claro.project`, making `package doctor` reject stale lockfile checksums for listed packages, and making `package doctor` reject lockfile package entries that are not listed in `claro.project`. Current function validation covers correct checked calls, wrong-type arguments, missing checked-argument diagnostics, missing unchecked-argument diagnostics for simple functions in both modern `DO greet` and empty compatibility `CALL greet WITH` forms, extra-argument diagnostics, and beginner-facing unknown-function diagnostics for mistyped modern `DO` and compatibility `CALL ... WITH` calls; current object-method parameter validation covers correct modern `DO object.method ...` and compatibility `CALL object.method WITH ...` checked calls, wrong-type diagnostics for both call forms, checked methods after another method in the same class, missing and extra checked-argument diagnostics including the extra-argument case for a checked method after another method in the same class, missing unchecked-argument diagnostics for simple object methods, missing-object guidance when `DO player.method ...` or compatibility `CALL object.method WITH ...` appears before `NEW` even if the method name is also wrong, and class-specific unknown-method diagnostics for both modern and compatibility calls when a learner calls a method the class does not declare; current object-field validation covers direct NUMBER/TEXT/YESNO field-assignment positives, direct object-field `CHECK TYPE` metadata positives for NUMBER/TEXT/YESNO fields, negative NUMBER/TEXT/YESNO field metadata mismatches, NUMBER/TEXT/YESNO-expectation unknown-field `CHECK TYPE` diagnostics, direct wrong-type field assignment diagnostics, field collection when a `HAS` field appears after a simple method, direct unknown-field diagnostics for NUMBER/TEXT/YESNO values, a beginner-facing fallback for unknown fields assigned from expressions whose type is not inferable yet, and missing-object diagnostics for both `SET object.field value` and `CHECK TYPE object.field IS TYPE` before `NEW`. 3a. Keep package validation honest by checking that the project and each listed package manifest declare the exact supported manifest version, package version, local source, and expected package name. -3b. Keep package security validation in Forgejo/Gitea CI so unsafe names, exact manifest-version/name/checksum mismatches, missing manifests, duplicate project packages, and lockfile errors remain release blockers. +3b. Keep package security validation in Forgejo/Gitea CI so unsafe names, exact manifest-version/name/checksum mismatches, missing manifests, duplicate project packages, duplicate lock packages, and lockfile errors remain release blockers. 4. Add small examples for each foundation feature before adding bigger syntax. ## Complete-platform milestones diff --git a/docs/V1_16_PACKAGES_PROJECTS.md b/docs/V1_16_PACKAGES_PROJECTS.md index be44d17..71ea217 100644 --- a/docs/V1_16_PACKAGES_PROJECTS.md +++ b/docs/V1_16_PACKAGES_PROJECTS.md @@ -63,7 +63,7 @@ source: local checksum: 1234abcd ``` -`claro.lock` records the release version, lock format, packages, and checksums so future registry work has a stable safety foundation. `claro package doctor` checks those lockfile checksums for listed packages and reports `BAD lock checksum: name` if the lockfile is stale or edited incorrectly. It also reports `BAD lock package not in claro.project: name` when the lockfile contains a package entry that is not listed in `claro.project`, so learners know to refresh the lockfile instead of trusting stale package data. The project file must declare the complete supported `manifest-version: 1` value; a prefix such as `manifest-version: 10` is rejected as `BAD project manifest version: expected 1`. Package manifests must declare the complete supported `manifest-version: 1` value; a prefix such as `manifest-version: 10` is treated as a missing/unsupported manifest. They must also declare the complete expected `name:` value; a name that only starts with the package name is rejected as `BAD package manifest name: name`. The complete manifest `checksum:` value is checked too, so trailing or extra checksum text is rejected as `BAD package checksum: name`. Package manifests must also declare the complete supported local `version: 1` value and `source: local` value. Prefixes such as `version: 10` and `source: local-extra` are rejected as `BAD package version: name` and `BAD package source: name` rather than being accepted as valid local manifests. +`claro.lock` records the release version, lock format, packages, and checksums so future registry work has a stable safety foundation. `claro package doctor` checks those lockfile checksums for listed packages and reports `BAD lock checksum: name` if the lockfile is stale or edited incorrectly. It rejects duplicate `package:` entries with `DUPLICATE lock package: name`, so one package cannot have ambiguous lock data. It also reports `BAD lock package not in claro.project: name` when the lockfile contains a package entry that is not listed in `claro.project`, so learners know to refresh the lockfile instead of trusting stale package data. The project file must declare the complete supported `manifest-version: 1` value; a prefix such as `manifest-version: 10` is rejected as `BAD project manifest version: expected 1`. Package manifests must declare the complete supported `manifest-version: 1` value; a prefix such as `manifest-version: 10` is treated as a missing/unsupported manifest. They must also declare the complete expected `name:` value; a name that only starts with the package name is rejected as `BAD package manifest name: name`. The complete manifest `checksum:` value is checked too, so trailing or extra checksum text is rejected as `BAD package checksum: name`. Package manifests must also declare the complete supported local `version: 1` value and `source: local` value. Prefixes such as `version: 10` and `source: local-extra` are rejected as `BAD package version: name` and `BAD package source: name` rather than being accepted as valid local manifests. ## Project-name safety diff --git a/src/claro.c b/src/claro.c index da2a15d..2d89abd 100644 --- a/src/claro.c +++ b/src/claro.c @@ -637,7 +637,7 @@ static int remove_package_from_project(const char *name){ char *txt=read_file_te static void create_package_folder(const char *name){ char path[512], meta[768], readme[768], sum[32]; make_folder("packages"); snprintf(path,sizeof(path),"packages/%s",name); make_folder(path); snprintf(meta,sizeof(meta),"%s/claro.package",path); package_checksum(name,sum,sizeof(sum)); { char text[1024]; snprintf(text,sizeof(text),"manifest-version: 1\nname: %s\nversion: 1\nsource: local\nchecksum: %s\n",name,sum); write_text_file_simple(meta,text); } snprintf(readme,sizeof(readme),"%s/README.md",path); if(!file_exists_simple(readme)){ char text[512]; snprintf(text,sizeof(text),"# %s\n\nThis is a local Claro package folder.\n",name); write_text_file_simple(readme,text); } } static int list_project_packages(void){ char *txt=read_file_text("claro.project"); char *p; int count=0; printf("Packages in claro.project:\n"); if(!txt){ printf(" (no claro.project yet)\n"); return 0; } if(!project_package_names_safe()){ free(txt); return 1; } p=txt; while(*p){ char *line=p; char save; while(*p&&*p!='\n'&&*p!='\r') p++; save=*p; *p=0; { char *t=trim_inplace(line); if(strnicmp2(t,"package:",8)==0){ char *pkg=trim_inplace(t+8); if(*pkg){ printf(" %s\n",pkg); count++; } } } if(save) p++; while(*p=='\n'||*p=='\r') p++; } if(!count) printf(" (none)\n"); free(txt); return 0; } static int package_lock_checksum_ok(const char *name){ char *txt=read_file_text("claro.lock"); char *p; int in_pkg=0, ok=0; char expected[32]; if(!txt) return 0; package_checksum(name,expected,sizeof(expected)); p=txt; while(*p){ char *line=p; char save; while(*p&&*p!='\n'&&*p!='\r') p++; save=*p; *p=0; { char *t=trim_inplace(line); if(strnicmp2(t,"package:",8)==0){ char *pkg=trim_inplace(t+8); in_pkg=(strcmp(pkg,name)==0); } else if(in_pkg&&strnicmp2(t,"checksum:",9)==0){ char *sum=trim_inplace(t+9); ok=(strcmp(sum,expected)==0); break; } } if(save){ *p=save; p++; } while(*p=='\n'||*p=='\r') p++; } free(txt); return ok; } -static int lock_packages_match_project(void){ char *txt=read_file_text("claro.lock"); char *p; int ok=1; if(!txt) return 1; p=txt; while(*p){ char *line=p; char save; while(*p&&*p!='\n'&&*p!='\r') p++; save=*p; *p=0; { char *t=trim_inplace(line); if(strnicmp2(t,"package:",8)==0){ char *pkg=trim_inplace(t+8); if(*pkg&&!package_name_safe(pkg)){ printf(" BAD lock package name: %s\n",pkg); ok=0; } else if(*pkg&&!package_has_name(pkg)){ printf(" BAD lock package not in claro.project: %s\n",pkg); ok=0; } } } if(save){ *p=save; p++; } while(*p=='\n'||*p=='\r') p++; } free(txt); return ok; } +static int lock_packages_match_project(void){ char *txt=read_file_text("claro.lock"); char *p; char seen[128][65]; int seen_count=0; int ok=1; if(!txt) return 1; p=txt; while(*p){ char *line=p; char save; while(*p&&*p!='\n'&&*p!='\r') p++; save=*p; *p=0; { char *t=trim_inplace(line); if(strnicmp2(t,"package:",8)==0){ char *pkg=trim_inplace(t+8); int i; if(*pkg&&!package_name_safe(pkg)){ printf(" BAD lock package name: %s\n",pkg); ok=0; } else if(*pkg){ for(i=0;i=17&&strncmp(line,"manifest-version:",17)==0){ const char *value=line+17; while(valuevalue&&isspace((unsigned char)end[-1])) end--; return (size_t)(end-value)==1&&value[0]=='1'; } while(*p=='\n'||*p=='\r') p++; } return 0; } static int package_manifest_name_matches(const char *text,const char *name){ const char *p=text; size_t n=strlen(name); while(p&&*p){ const char *line=p; const char *end; while(*p&&*p!='\n'&&*p!='\r') p++; end=p; if((size_t)(end-line)>=5&&strncmp(line,"name:",5)==0){ const char *value=line+5; while(valuevalue&&isspace((unsigned char)end[-1])) end--; if((size_t)(end-value)==n&&strncmp(value,name,n)==0) return 1; } while(*p=='\n'||*p=='\r') p++; } return 0; } static int package_manifest_version_value_matches(const char *text){ const char *p=text; while(p&&*p){ const char *line=p; const char *end; while(*p&&*p!='\n'&&*p!='\r') p++; end=p; if((size_t)(end-line)>=8&&strncmp(line,"version:",8)==0){ const char *value=line+8; while(valuevalue&&isspace((unsigned char)end[-1])) end--; return (size_t)(end-value)==1&&value[0]=='1'; } while(*p=='\n'||*p=='\r') p++; } return 0; } diff --git a/tools/validate_package_security.py b/tools/validate_package_security.py index dcc0b10..7206bae 100644 --- a/tools/validate_package_security.py +++ b/tools/validate_package_security.py @@ -185,6 +185,17 @@ def main(): fail(f"package doctor orphan-lock diagnostic was unclear:\n{out}") (work / "claro.lock").write_text(lock, encoding="utf-8") + (work / "claro.lock").write_text( + lock + "package: math-tools\nchecksum: " + checksum + "\n", + encoding="utf-8", + ) + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail("package doctor must reject duplicate lockfile package entries") + if "DUPLICATE lock package: math-tools" not in out: + fail(f"package doctor duplicate-lock diagnostic was unclear:\n{out}") + (work / "claro.lock").write_text(lock, encoding="utf-8") + (work / "claro.project").write_text( project + "package: math-tools\npackage: math-tools\n", encoding="utf-8",