package: reject duplicate lockfile checksums

This commit is contained in:
Hermes Agent
2026-09-05 05:18:41 +00:00
parent 6739a82f5e
commit 023badf016
3 changed files with 19 additions and 1 deletions
+2
View File
@@ -67,6 +67,8 @@ checksum: 1234abcd
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.
If `package doctor` reports `BAD lock checksum: math-tools`, check the `math-tools` entry in `claro.lock`. Each package must have exactly one matching `checksum:` line. Duplicate checksums are rejected even when they agree, or when a correct checksum appears before or after an incorrect one. The doctor leaves the file unchanged. After reviewing your project and package manifests, run `claro package lock` to regenerate the lockfile, then run `claro package doctor` again.
## 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.
+1 -1
View File
@@ -636,7 +636,7 @@ static int append_package_to_project(const char *name){ FILE *f; if(package_name
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); if(!write_package_lock()) return -1; return removed; }
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 package_lock_checksum_ok(const char *name){ char *txt=read_file_text("claro.lock"); char *p; int in_pkg=0, fields=0, matches=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); fields++; if(strcmp(sum,expected)==0) matches++; } } if(save){ *p=save; p++; } while(*p=='\n'||*p=='\r') p++; } free(txt); return fields==1&&matches==1; }
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<seen_count;i++){ if(strcmp(seen[i],pkg)==0){ printf(" DUPLICATE lock package: %s\n",pkg); ok=0; break; } } if(seen_count<128) snprintf(seen[seen_count++],sizeof(seen[0]),"%s",pkg); if(!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 package_manifest_version_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)>=17&&strncmp(line,"manifest-version:",17)==0){ const char *value=line+17; 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_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; }
+16
View File
@@ -115,6 +115,22 @@ def main():
fail(f"package doctor stale-lock diagnostic was unclear:\n{out}")
(work / "claro.lock").write_text(lock, encoding="utf-8")
for sums in [(checksum, checksum), (checksum, "bad"), ("bad", checksum)]:
duplicate_lock = lock.replace(
"checksum: " + checksum,
"\n".join("checksum: " + value for value in sums),
1,
)
(work / "claro.lock").write_text(duplicate_lock, encoding="utf-8")
rc, out = run([str(EXE), "package", "doctor"], work)
if rc == 0:
fail(f"package doctor must reject duplicate lock checksums: {sums}")
if "BAD lock checksum: math-tools" not in out:
fail(f"Duplicate lock checksum diagnostic was unclear:\n{out}")
if read(work / "claro.lock") != duplicate_lock:
fail("package doctor must not rewrite a lockfile with duplicate checksums")
(work / "claro.lock").write_text(lock, encoding="utf-8")
checksum_line = next((line for line in manifest.splitlines() if line.startswith("checksum:")), "")
checksum = checksum_line.split(":", 1)[1].strip()
project_with_package = read(work / "claro.project")