package: reject duplicate manifest sources

This commit is contained in:
Hermes Agent
2026-09-05 01:14:24 +00:00
parent 8f449898bd
commit 6739a82f5e
3 changed files with 17 additions and 1 deletions
+2
View File
@@ -65,6 +65,8 @@ 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 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 exactly one expected `name:` value; duplicate or prefix-matching names are rejected as `BAD package manifest name: name`. The manifest must contain exactly one checksum field with the expected complete value; duplicate, trailing, or extra checksum text is rejected as `BAD package checksum: name`. Package manifests must also declare exactly one `version: 1` field; duplicate or prefix-matching versions such as `version: 10` are rejected as `BAD package version: name`. Package manifests must also declare the complete supported local `source: local` value. Prefixes such as `source: local-extra` are rejected as `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 exactly one expected `name:` value; duplicate or prefix-matching names are rejected as `BAD package manifest name: name`. The manifest must contain exactly one checksum field with the expected complete value; duplicate, trailing, or extra checksum text is rejected as `BAD package checksum: name`. Package manifests must also declare exactly one `version: 1` field; duplicate or prefix-matching versions such as `version: 10` are rejected as `BAD package version: name`. Package manifests must also declare the complete supported local `source: local` value. Prefixes such as `source: local-extra` are rejected as `BAD package source: name` rather than being accepted as valid local manifests.
If `package doctor` reports `BAD package source: math-tools`, open `packages/math-tools/claro.package` and keep exactly one `source: local` line. Duplicate `source:` lines are rejected even when both say `local`, or when a valid line appears before or after an unsupported source. The doctor leaves the file unchanged so you can review and repair it yourself.
## Project-name safety ## Project-name safety
`claro new NAME` checks the project name before creating folders. Project names may use only letters, numbers, dash, and underscore, and they must be 64 characters or fewer. `claro new NAME` checks the project name before creating folders. Project names may use only letters, numbers, dash, and underscore, and they must be 64 characters or fewer.
+1 -1
View File
@@ -642,7 +642,7 @@ static int package_manifest_version_matches(const char *text){ const char *p=tex
static int package_project_name_valid(const char *text){ const char *p=text; int fields=0; int nonempty=0; 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; fields++; while(value<end&&isspace((unsigned char)*value)) value++; while(end>value&&isspace((unsigned char)end[-1])) end--; if(value<end) nonempty++; } while(*p=='\n'||*p=='\r') p++; } return fields==1&&nonempty==1; } static int package_project_name_valid(const char *text){ const char *p=text; int fields=0; int nonempty=0; 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; fields++; while(value<end&&isspace((unsigned char)*value)) value++; while(end>value&&isspace((unsigned char)end[-1])) end--; if(value<end) nonempty++; } while(*p=='\n'||*p=='\r') p++; } return fields==1&&nonempty==1; }
static int package_manifest_name_matches(const char *text,const char *name){ const char *p=text; size_t n=strlen(name); int fields=0; int matches=0; 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; fields++; while(value<end&&isspace((unsigned char)*value)) value++; while(end>value&&isspace((unsigned char)end[-1])) end--; if((size_t)(end-value)==n&&strncmp(value,name,n)==0) matches++; } while(*p=='\n'||*p=='\r') p++; } return fields==1&&matches==1; } static int package_manifest_name_matches(const char *text,const char *name){ const char *p=text; size_t n=strlen(name); int fields=0; int matches=0; 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; fields++; while(value<end&&isspace((unsigned char)*value)) value++; while(end>value&&isspace((unsigned char)end[-1])) end--; if((size_t)(end-value)==n&&strncmp(value,name,n)==0) matches++; } while(*p=='\n'||*p=='\r') p++; } return fields==1&&matches==1; }
static int package_manifest_version_value_matches(const char *text){ const char *p=text; int fields=0; int matches=0; 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; fields++; while(value<end&&isspace((unsigned char)*value)) value++; while(end>value&&isspace((unsigned char)end[-1])) end--; if((size_t)(end-value)==1&&value[0]=='1') matches++; } while(*p=='\n'||*p=='\r') p++; } return fields==1&&matches==1; } static int package_manifest_version_value_matches(const char *text){ const char *p=text; int fields=0; int matches=0; 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; fields++; while(value<end&&isspace((unsigned char)*value)) value++; while(end>value&&isspace((unsigned char)end[-1])) end--; if((size_t)(end-value)==1&&value[0]=='1') matches++; } while(*p=='\n'||*p=='\r') p++; } return fields==1&&matches==1; }
static int package_manifest_source_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)>=7&&strncmp(line,"source:",7)==0){ const char *value=line+7; while(value<end&&isspace((unsigned char)*value)) value++; while(end>value&&isspace((unsigned char)end[-1])) end--; return (size_t)(end-value)==5&&strncmp(value,"local",5)==0; } while(*p=='\n'||*p=='\r') p++; } return 0; } static int package_manifest_source_matches(const char *text){ const char *p=text; int fields=0; int matches=0; while(p&&*p){ const char *line=p; const char *end; while(*p&&*p!='\n'&&*p!='\r') p++; end=p; if((size_t)(end-line)>=7&&strncmp(line,"source:",7)==0){ const char *value=line+7; fields++; while(value<end&&isspace((unsigned char)*value)) value++; while(end>value&&isspace((unsigned char)end[-1])) end--; if((size_t)(end-value)==5&&strncmp(value,"local",5)==0) matches++; } while(*p=='\n'||*p=='\r') p++; } return fields==1&&matches==1; }
static int package_manifest_checksum_matches(const char *text,const char *expected){ const char *p=text; size_t n=strlen(expected); int fields=0; int matches=0; while(p&&*p){ const char *line=p; const char *end; while(*p&&*p!='\n'&&*p!='\r') p++; end=p; if((size_t)(end-line)>=9&&strncmp(line,"checksum:",9)==0){ const char *value=line+9; fields++; while(value<end&&isspace((unsigned char)*value)) value++; while(end>value&&isspace((unsigned char)end[-1])) end--; if((size_t)(end-value)==n&&strncmp(value,expected,n)==0) matches++; } while(*p=='\n'||*p=='\r') p++; } return fields==1&&matches==1; } static int package_manifest_checksum_matches(const char *text,const char *expected){ const char *p=text; size_t n=strlen(expected); int fields=0; int matches=0; while(p&&*p){ const char *line=p; const char *end; while(*p&&*p!='\n'&&*p!='\r') p++; end=p; if((size_t)(end-line)>=9&&strncmp(line,"checksum:",9)==0){ const char *value=line+9; fields++; while(value<end&&isspace((unsigned char)*value)) value++; while(end>value&&isspace((unsigned char)end[-1])) end--; if((size_t)(end-value)==n&&strncmp(value,expected,n)==0) matches++; } while(*p=='\n'||*p=='\r') p++; } return fields==1&&matches==1; }
static int package_doctor(void){ int ok=1; char *mainfile; char *txt; printf("%s\n",CLARO_VERSION); printf("Package/project doctor:\n"); printf(" %s claro.project\n",file_exists_simple("claro.project")?"OK":"MISSING"); if(!file_exists_simple("claro.project")) ok=0; mainfile=project_value("main"); if(mainfile&&*mainfile){ printf(" %s main script: %s\n",file_exists_simple(mainfile)?"OK":"MISSING",mainfile); if(!file_exists_simple(mainfile)) ok=0; } else { printf(" MISSING main setting\n"); ok=0; } printf(" %s packages folder\n",path_exists_simple("packages")?"OK":"MISSING"); if(!path_exists_simple("packages")) ok=0; printf(" %s claro.lock\n",file_exists_simple("claro.lock")?"OK":"MISSING"); if(!file_exists_simple("claro.lock")) ok=0; txt=read_file_text("claro.project"); if(txt&&!project_package_names_safe()){ ok=0; } if(txt&& !package_manifest_version_matches(txt)){ printf(" BAD project manifest version: expected 1\n"); ok=0; } if(txt&& !package_project_name_valid(txt)){ printf(" BAD project manifest name: expected a non-empty name\n"); ok=0; } if(txt){ char *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 meta[512], *mt, sum[32]; if(!package_name_safe(pkg)){ printf(" BAD package name: %s\n",pkg); ok=0; } else { snprintf(meta,sizeof(meta),"packages/%s/claro.package",pkg); mt=read_file_text(meta); if(mt&&!package_manifest_version_value_matches(mt)){ printf(" BAD package version: %s\n",pkg); ok=0; } else if(mt&&!package_manifest_source_matches(mt)){ printf(" BAD package source: %s\n",pkg); ok=0; } else if(mt&&package_manifest_version_matches(mt)&&strstr(mt,"checksum:")){ if(!package_manifest_name_matches(mt,pkg)){ printf(" BAD package manifest name: %s\n",pkg); ok=0; } else { printf(" OK package manifest: %s\n",pkg); package_checksum(pkg,sum,sizeof(sum)); if(package_manifest_checksum_matches(mt,sum)) printf(" OK package checksum: %s\n",pkg); else { printf(" BAD package checksum: %s\n",pkg); ok=0; } if(package_lock_checksum_ok(pkg)) printf(" OK lock checksum: %s\n",pkg); else { printf(" BAD lock checksum: %s\n",pkg); ok=0; } } } else { printf(" MISSING package manifest: %s\n",pkg); ok=0; } free(mt); } } } } if(save) p++; while(*p=='\n'||*p=='\r') p++; } free(txt); } if(!lock_packages_match_project()) ok=0; free(mainfile); printf("%s\n",ok?"Package/project files look ready.":"Package/project files need attention."); return ok?0:1; } static int package_doctor(void){ int ok=1; char *mainfile; char *txt; printf("%s\n",CLARO_VERSION); printf("Package/project doctor:\n"); printf(" %s claro.project\n",file_exists_simple("claro.project")?"OK":"MISSING"); if(!file_exists_simple("claro.project")) ok=0; mainfile=project_value("main"); if(mainfile&&*mainfile){ printf(" %s main script: %s\n",file_exists_simple(mainfile)?"OK":"MISSING",mainfile); if(!file_exists_simple(mainfile)) ok=0; } else { printf(" MISSING main setting\n"); ok=0; } printf(" %s packages folder\n",path_exists_simple("packages")?"OK":"MISSING"); if(!path_exists_simple("packages")) ok=0; printf(" %s claro.lock\n",file_exists_simple("claro.lock")?"OK":"MISSING"); if(!file_exists_simple("claro.lock")) ok=0; txt=read_file_text("claro.project"); if(txt&&!project_package_names_safe()){ ok=0; } if(txt&& !package_manifest_version_matches(txt)){ printf(" BAD project manifest version: expected 1\n"); ok=0; } if(txt&& !package_project_name_valid(txt)){ printf(" BAD project manifest name: expected a non-empty name\n"); ok=0; } if(txt){ char *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 meta[512], *mt, sum[32]; if(!package_name_safe(pkg)){ printf(" BAD package name: %s\n",pkg); ok=0; } else { snprintf(meta,sizeof(meta),"packages/%s/claro.package",pkg); mt=read_file_text(meta); if(mt&&!package_manifest_version_value_matches(mt)){ printf(" BAD package version: %s\n",pkg); ok=0; } else if(mt&&!package_manifest_source_matches(mt)){ printf(" BAD package source: %s\n",pkg); ok=0; } else if(mt&&package_manifest_version_matches(mt)&&strstr(mt,"checksum:")){ if(!package_manifest_name_matches(mt,pkg)){ printf(" BAD package manifest name: %s\n",pkg); ok=0; } else { printf(" OK package manifest: %s\n",pkg); package_checksum(pkg,sum,sizeof(sum)); if(package_manifest_checksum_matches(mt,sum)) printf(" OK package checksum: %s\n",pkg); else { printf(" BAD package checksum: %s\n",pkg); ok=0; } if(package_lock_checksum_ok(pkg)) printf(" OK lock checksum: %s\n",pkg); else { printf(" BAD lock checksum: %s\n",pkg); ok=0; } } } else { printf(" MISSING package manifest: %s\n",pkg); ok=0; } free(mt); } } } } if(save) p++; while(*p=='\n'||*p=='\r') p++; } free(txt); } if(!lock_packages_match_project()) ok=0; free(mainfile); printf("%s\n",ok?"Package/project files look ready.":"Package/project files need attention."); return ok?0:1; }
static int run_package_cmd(int argc,char **argv,int arg){ if(arg>=argc){ printf("Package commands:\n claro package init\n claro package add NAME\n claro package remove NAME\n claro package list\n claro package doctor\n claro package lock\n"); return 0; } if(strcmp(argv[arg],"init")==0){ ensure_project_file(); if(!project_package_names_safe()) return 1; make_folder("packages"); if(!write_package_lock()) return 1; printf("Package project ready: claro.project, claro.lock, packages/\n"); return 0; } if(strcmp(argv[arg],"add")==0&&arg+1<argc){ const char *name=argv[arg+1]; int already; if(package_name_too_long(name)){ fprintf(stderr,"Package names must be 64 characters or fewer.\n"); return 1; } if(!package_name_safe(name)){ fprintf(stderr,"Package names may use only letters, numbers, dash, and underscore.\n"); return 1; } ensure_project_file(); if(!project_package_names_safe()) return 1; make_folder("packages"); already=package_has_name(name); if(!append_package_to_project(name)) return 1; create_package_folder(name); if(!write_package_lock()) return 1; if(!already) printf("Added package: %s\n",name); return 0; } if(strcmp(argv[arg],"remove")==0&&arg+1<argc){ const char *name=argv[arg+1]; int removed; removed=remove_package_from_project(name); if(removed<0) return 1; if(removed) printf("Removed package from project: %s\n",name); else printf("Package was not listed: %s\n",name); return 0; } if(strcmp(argv[arg],"list")==0) return list_project_packages(); if(strcmp(argv[arg],"doctor")==0) return package_doctor(); if(strcmp(argv[arg],"lock")==0||strcmp(argv[arg],"update")==0){ ensure_project_file(); make_folder("packages"); if(write_package_lock()){ printf("Updated claro.lock\n"); return 0; } fprintf(stderr,"Could not write claro.lock\n"); return 1; } printf("Unknown package command. Try: claro package init\n"); return 1; } static int run_package_cmd(int argc,char **argv,int arg){ if(arg>=argc){ printf("Package commands:\n claro package init\n claro package add NAME\n claro package remove NAME\n claro package list\n claro package doctor\n claro package lock\n"); return 0; } if(strcmp(argv[arg],"init")==0){ ensure_project_file(); if(!project_package_names_safe()) return 1; make_folder("packages"); if(!write_package_lock()) return 1; printf("Package project ready: claro.project, claro.lock, packages/\n"); return 0; } if(strcmp(argv[arg],"add")==0&&arg+1<argc){ const char *name=argv[arg+1]; int already; if(package_name_too_long(name)){ fprintf(stderr,"Package names must be 64 characters or fewer.\n"); return 1; } if(!package_name_safe(name)){ fprintf(stderr,"Package names may use only letters, numbers, dash, and underscore.\n"); return 1; } ensure_project_file(); if(!project_package_names_safe()) return 1; make_folder("packages"); already=package_has_name(name); if(!append_package_to_project(name)) return 1; create_package_folder(name); if(!write_package_lock()) return 1; if(!already) printf("Added package: %s\n",name); return 0; } if(strcmp(argv[arg],"remove")==0&&arg+1<argc){ const char *name=argv[arg+1]; int removed; removed=remove_package_from_project(name); if(removed<0) return 1; if(removed) printf("Removed package from project: %s\n",name); else printf("Package was not listed: %s\n",name); return 0; } if(strcmp(argv[arg],"list")==0) return list_project_packages(); if(strcmp(argv[arg],"doctor")==0) return package_doctor(); if(strcmp(argv[arg],"lock")==0||strcmp(argv[arg],"update")==0){ ensure_project_file(); make_folder("packages"); if(write_package_lock()){ printf("Updated claro.lock\n"); return 0; } fprintf(stderr,"Could not write claro.lock\n"); return 1; } printf("Unknown package command. Try: claro package init\n"); return 1; }
+14
View File
@@ -156,6 +156,20 @@ def main():
fail(f"Package manifest source diagnostic was unclear:\n{out}") fail(f"Package manifest source diagnostic was unclear:\n{out}")
manifest_path.write_text(manifest, encoding="utf-8") manifest_path.write_text(manifest, encoding="utf-8")
for sources in [("local", "local"), ("local", "remote"), ("remote", "local")]:
duplicate_source_manifest = manifest.replace(
"source: local", "\n".join("source: " + source for source in sources), 1
)
manifest_path.write_text(duplicate_source_manifest, encoding="utf-8")
rc, out = run([str(EXE), "package", "doctor"], work)
if rc == 0:
fail(f"package doctor must reject duplicate package manifest sources: {sources}")
if "BAD package source: math-tools" not in out:
fail(f"Package duplicate source diagnostic was unclear:\n{out}")
if read(manifest_path) != duplicate_source_manifest:
fail("package doctor must not rewrite a manifest with duplicate sources")
manifest_path.write_text(manifest, encoding="utf-8")
duplicate_name_manifest = manifest + "name: other-tools\n" duplicate_name_manifest = manifest + "name: other-tools\n"
manifest_path.write_text(duplicate_name_manifest, encoding="utf-8") manifest_path.write_text(duplicate_name_manifest, encoding="utf-8")
rc, out = run([str(EXE), "package", "doctor"], work) rc, out = run([str(EXE), "package", "doctor"], work)