diff --git a/README.md b/README.md index a934478..07d5190 100644 --- a/README.md +++ b/README.md @@ -282,7 +282,7 @@ claro.lock packages/ ``` -Package names are checked so unsafe names such as `../bad` are rejected when adding packages, and `claro package doctor` also flags unsafe package names if they are already present in `claro.project`. +Package names are checked so unsafe names such as `../bad` are rejected when adding packages. If an unsafe name is already present in `claro.project`, both `claro package doctor` and `claro package lock` flag it instead of treating it as safe lockfile data. ## Standard-library path and collection helpers diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index d268114..d7c025d 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -96,7 +96,7 @@ Ready now: - `claro package init` - `claro package add/list/remove/doctor/lock` - local project files such as `claro.project`, `claro.lock`, and `packages/` -- package-name safety checks when adding packages and when `claro package doctor` audits an existing `claro.project` +- package-name safety checks when adding packages, when `claro package doctor` audits an existing `claro.project`, and when `claro package lock` writes lockfile data Still needed: - install from local path diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index a748937..dc330cb 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -31,7 +31,7 @@ See `CURRENT_STATUS.md` for the detailed feature matrix. 1. Keep beginner-facing docs current and separate from historical release notes. 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, and CI workflow coverage validation). Current package safety validation covers safe manifest/lockfile creation, rejecting unsafe package names during `package add`, and detecting unsafe package names already present in `claro.project` during `package doctor`. Current object-method parameter validation covers one correct `DO object.method ...` call, one wrong-type diagnostic, missing-object guidance when `DO player.method ...` or compatibility `CALL object.method WITH ...` appears before `NEW`, and a class-specific unknown-method diagnostic when a learner calls a method the class does not declare; object-field validation covers correct NUMBER, TEXT, and YESNO direct `SET object.field value` assignments, direct `CHECK TYPE` metadata acceptance for NUMBER/TEXT/YESNO fields, negative NUMBER/TEXT/YESNO metadata mismatches, NUMBER/TEXT/YESNO-expectation unknown-field `CHECK TYPE` diagnostics, missing-object field assignment and `CHECK TYPE` diagnostics, NUMBER/TEXT/YESNO wrong-type direct assignments, simple NUMBER/TEXT/YESNO-valued unknown-field diagnostics after `NEW Class object`, and a beginner-facing fallback when the unknown field's assigned expression type is not inferable yet. +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, and CI workflow coverage validation). Current package safety validation covers safe manifest/lockfile creation, rejecting unsafe package names during `package add`, detecting unsafe package names already present in `claro.project` during `package doctor`, and refusing to write lockfile data for unsafe package names during `package lock`. Current object-method parameter validation covers one correct `DO object.method ...` call, one wrong-type diagnostic, missing-object guidance when `DO player.method ...` or compatibility `CALL object.method WITH ...` appears before `NEW`, and a class-specific unknown-method diagnostic when a learner calls a method the class does not declare; object-field validation covers correct NUMBER, TEXT, and YESNO direct `SET object.field value` assignments, direct `CHECK TYPE` metadata acceptance for NUMBER/TEXT/YESNO fields, negative NUMBER/TEXT/YESNO metadata mismatches, NUMBER/TEXT/YESNO-expectation unknown-field `CHECK TYPE` diagnostics, missing-object field assignment and `CHECK TYPE` diagnostics, NUMBER/TEXT/YESNO wrong-type direct assignments, simple NUMBER/TEXT/YESNO-valued unknown-field diagnostics after `NEW Class object`, and a beginner-facing fallback when the unknown field's assigned expression type is not inferable yet. 4. Add small examples for each foundation feature before adding bigger syntax. ## Complete-platform milestones diff --git a/src/claro.c b/src/claro.c index 0e4cbdb..ee98617 100644 --- a/src/claro.c +++ b/src/claro.c @@ -582,7 +582,7 @@ static void package_checksum(const char *name,char *out,size_t n){ unsigned long static void ensure_project_file(void){ if(!file_exists_simple("claro.project")) write_text_file_simple("claro.project","manifest-version: 1\nname: ClaroProject\nmain: main.claro\nversion: v1.18.26\npackages:\n"); } static char *project_value(const char *key){ char *txt=read_file_text("claro.project"); size_t kn=strlen(key); char *p,*out=NULL; if(!txt) return NULL; 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,key,kn)==0 && t[kn]==':'){ out=xstrdup(trim_inplace(t+kn+1)); break; } } if(save) p++; while(*p=='\n'||*p=='\r') p++; } free(txt); return out; } static int package_has_name(const char *name){ char *txt=read_file_text("claro.project"); char *p; int found=0; if(!txt) return 0; 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(strcmp(pkg,name)==0){ found=1; break; } } } if(save) p++; while(*p=='\n'||*p=='\r') p++; } free(txt); return found; } -static int write_package_lock(void){ char *txt=read_file_text("claro.project"); FILE *f; char *p; if(!txt) return 0; f=fopen("claro.lock","wb"); if(!f){ free(txt); return 0; } fputs("# Claro package lock\nlock-version: 1\nversion: v1.18.26\n",f); 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){ char sum[32]; package_checksum(pkg,sum,sizeof(sum)); fprintf(f,"package: %s\nchecksum: %s\n",pkg,sum); } } } if(save) p++; while(*p=='\n'||*p=='\r') p++; } fclose(f); free(txt); return 1; } +static int write_package_lock(void){ char *txt=read_file_text("claro.project"); FILE *f; char *p; if(!txt) return 0; 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 package name: %s\n",pkg); free(txt); return 0; } } } if(save){ *p=save; p++; } while(*p=='\n'||*p=='\r') p++; } f=fopen("claro.lock","wb"); if(!f){ free(txt); return 0; } fputs("# Claro package lock\nlock-version: 1\nversion: v1.18.26\n",f); 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){ char sum[32]; package_checksum(pkg,sum,sizeof(sum)); fprintf(f,"package: %s\nchecksum: %s\n",pkg,sum); } } } if(save) p++; while(*p=='\n'||*p=='\r') p++; } fclose(f); free(txt); return 1; } static int append_package_to_project(const char *name){ FILE *f; if(!package_name_safe(name)){ fprintf(stderr,"Package names may use only letters, numbers, dash, and underscore.\n"); return 0; } ensure_project_file(); if(package_has_name(name)){ printf("Package already listed: %s\n",name); return 1; } f=fopen("claro.project","ab"); if(!f) return 0; fprintf(f,"package: %s\n",name); fclose(f); write_package_lock(); return 1; } static int remove_package_from_project(const char *name){ char *txt=read_file_text("claro.project"); FILE *f; char *p; int removed=0; if(!txt) return 0; f=fopen("claro.project","wb"); if(!f){ free(txt); return 0; } p=txt; while(*p){ char *line=p; char save; int skip=0; while(*p&&*p!='\n'&&*p!='\r') p++; save=*p; *p=0; { char *copy=xstrdup(line); char *t=trim_inplace(copy); if(strnicmp2(t,"package:",8)==0){ char *pkg=trim_inplace(t+8); if(strcmp(pkg,name)==0){ skip=1; removed=1; } } free(copy); } if(!skip) fprintf(f,"%s\n",line); if(save) p++; while(*p=='\n'||*p=='\r') p++; } fclose(f); free(txt); write_package_lock(); return removed; } static void create_package_folder(const char *name){ char path[512], meta[512], readme[512], 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); } } diff --git a/tools/validate_package_security.py b/tools/validate_package_security.py index 61238cd..a9810bd 100644 --- a/tools/validate_package_security.py +++ b/tools/validate_package_security.py @@ -83,6 +83,12 @@ def main(): if "BAD package name: ../bad" not in out: fail(f"package doctor unsafe-name diagnostic was unclear:\n{out}") + rc, out = run([str(EXE), "package", "lock"], work) + if rc == 0: + fail("package lock must reject unsafe package names already present in claro.project") + if "BAD package name: ../bad" not in out: + fail(f"package lock unsafe-name diagnostic was unclear:\n{out}") + print("Package security validation OK")