Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hemlang/hpm",
"version": "1.0.8",
"version": "1.0.9",
"description": "Hemlock Package Manager - A package manager for the Hemlock programming language",
"author": "Hemlock Team",
"license": "MIT",
Expand Down
49 changes: 25 additions & 24 deletions src/cache.hml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import { exists, read_file, write_file, make_dir, list_dir, remove_file, remove_
import * as env from "@stdlib/env";
import { parse, stringify, pretty } from "@stdlib/json";
import { exec } from "@stdlib/process";
import * as semver from "./semver.hml";

// Strip 'v' prefix from version string for cache paths
fn clean_version(version: string): string {
return semver.strip_v_prefix(version);
}

// Get cache directory path
export fn get_cache_dir(): string {
Expand Down Expand Up @@ -71,13 +77,7 @@ fn ensure_dir(path: string) {
// Get path to cached tarball
export fn get_cached_tarball_path(owner: string, repo: string, version: string): string {
let cache_dir = get_cache_dir();

// Remove 'v' prefix if present
let v = version;
if (v.length > 0 && v[0] == 'v') {
v = v.substr(1, v.length - 1);
}

let v = clean_version(version);
return cache_dir + "/" + owner + "/" + repo + "/" + v + ".tar.gz";
}

Expand All @@ -95,12 +95,7 @@ export fn get_cached_path(owner: string, repo: string, version: string): string
// Store tarball in cache from data
export fn store_tarball(owner: string, repo: string, version: string, data) {
let cache_dir = get_cache_dir();

// Remove 'v' prefix if present
let v = version;
if (v.length > 0 && v[0] == 'v') {
v = v.substr(1, v.length - 1);
}
let v = clean_version(version);

// Ensure directory structure
ensure_dir(cache_dir + "/" + owner);
Expand All @@ -111,10 +106,15 @@ export fn store_tarball(owner: string, repo: string, version: string, data) {

// Data might be string or buffer, handle both
let f = open(path, "w");
if (typeof(data) == "buffer") {
f.write_bytes(data);
} else {
f.write(data);
try {
if (typeof(data) == "buffer") {
f.write_bytes(data);
} else {
f.write(data);
}
} catch (e) {
f.close();
throw e;
}
f.close();

Expand All @@ -124,12 +124,7 @@ export fn store_tarball(owner: string, repo: string, version: string, data) {
// Store tarball in cache from a file (copies the file)
export fn store_tarball_file(owner: string, repo: string, version: string, src_path: string) {
let cache_dir = get_cache_dir();

// Remove 'v' prefix if present
let v = version;
if (v.length > 0 && v[0] == 'v') {
v = v.substr(1, v.length - 1);
}
let v = clean_version(version);

// Ensure directory structure
ensure_dir(cache_dir + "/" + owner);
Expand Down Expand Up @@ -157,7 +152,13 @@ export fn read_tarball(owner: string, repo: string, version: string) {
}

let f = open(path, "r");
let content = f.read();
let content = null;
try {
content = f.read();
} catch (e) {
f.close();
throw e;
}
f.close();

return content;
Expand Down
17 changes: 11 additions & 6 deletions src/github.hml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ fn github_request(url: string) {
}

headers.push("Accept: application/vnd.github.v3+json");
headers.push("User-Agent: hpm/1.0.7");
headers.push("User-Agent: hpm/1.0.9");

let retries = 0;
let backoff_ms = INITIAL_BACKOFF_MS;
Expand Down Expand Up @@ -252,10 +252,15 @@ export fn download_tarball(owner: string, repo: string, version: string): string

// Write binary data to file (use file handle for binary support)
let f = open(tmpfile, "w");
if (typeof(response.body) == "buffer") {
f.write_bytes(response.body);
} else {
f.write(response.body);
try {
if (typeof(response.body) == "buffer") {
f.write_bytes(response.body);
} else {
f.write(response.body);
}
} catch (e) {
f.close();
throw e;
}
f.close();

Expand All @@ -278,7 +283,7 @@ export fn repo_exists(owner: string, repo: string): bool {
}

headers.push("Accept: application/vnd.github.v3+json");
headers.push("User-Agent: hpm/1.0.7");
headers.push("User-Agent: hpm/1.0.9");

let retries = 0;
let backoff_ms = INITIAL_BACKOFF_MS;
Expand Down
53 changes: 24 additions & 29 deletions src/installer.hml
Original file line number Diff line number Diff line change
Expand Up @@ -88,23 +88,29 @@ export fn install_package(pkg_name: string, resolution, project_dir: string, ver
}
make_dir(extract_dir);

// Extract tarball using system tar
let tar_result = exec("tar", ["-xzf", tarball_path, "-C", extract_dir]);
if (tar_result.exit_code != 0) {
throw "Failed to extract tarball: tar exited with status " + tar_result.exit_code;
}
// Extract tarball using system tar (cleanup temp dir on any failure)
try {
let tar_result = exec("tar", ["-xzf", tarball_path, "-C", extract_dir]);
if (tar_result.exit_code != 0) {
throw "Failed to extract tarball: tar exited with status " + tar_result.exit_code;
}

// Find the extracted directory (GitHub tarballs have a root dir like "repo-version/")
let extracted_dirs = list_dir(extract_dir);
if (extracted_dirs.length == 0) {
throw "Failed to extract tarball: no files extracted";
}
let root_dir = extract_dir + "/" + extracted_dirs[0];
// Find the extracted directory (GitHub tarballs have a root dir like "repo-version/")
let extracted_dirs = list_dir(extract_dir);
if (extracted_dirs == null || extracted_dirs.length == 0) {
throw "Failed to extract tarball: no files extracted";
}
let root_dir = extract_dir + "/" + extracted_dirs[0];

// Copy contents to install_dir
let copy_result = exec("cp", ["-r", root_dir + "/.", install_dir]);
if (copy_result.exit_code != 0) {
throw "Failed to copy extracted files";
// Copy contents to install_dir
let copy_result = exec("cp", ["-r", root_dir + "/.", install_dir]);
if (copy_result.exit_code != 0) {
throw "Failed to copy extracted files";
}
} catch (e) {
// Clean up temp dir before re-throwing
remove_dir_recursive(extract_dir);
throw e;
}

// Clean up temp extraction dir
Expand All @@ -119,25 +125,14 @@ export fn install_package(pkg_name: string, resolution, project_dir: string, ver
integrity = "sha256-" + hash;
}
} catch (e) {
// Keep unknown integrity if calculation fails
if (verbose) {
print(" Warning: Could not calculate integrity hash: " + e);
}
}

return integrity;
}

// Helper to find last occurrence of character
fn find_last(s: string, ch: rune): i32 {
let last = -1;
let i = 0;
while (i < s.length) {
if (s[i] == ch) {
last = i;
}
i = i + 1;
}
return last;
}

// Ensure directory exists recursively
fn ensure_dir_recursive(path: string) {
if (exists(path)) {
Expand Down
60 changes: 5 additions & 55 deletions src/main.hml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import * as installer from "./installer.hml";
import * as cache from "./cache.hml";

// Version info
let HPM_VERSION = "1.0.7";
let HPM_VERSION = "1.0.9";

// Exit codes
let EXIT_SUCCESS = 0;
Expand Down Expand Up @@ -201,11 +201,7 @@ fn cmd_install() {
let versions = github.get_tags(parts.owner, parts.repo);
if (versions.length > 0) {
let sorted = semver.sort(versions);
let latest = sorted[sorted.length - 1];
// Remove 'v' prefix
if (latest.length > 0 && latest[0] == 'v') {
latest = latest.substr(1, latest.length - 1);
}
let latest = semver.strip_v_prefix(sorted[sorted.length - 1]);
version_constraint = "^" + latest;
}
}
Expand Down Expand Up @@ -606,17 +602,11 @@ fn cmd_outdated() {
let versions = github.get_tags(parts.owner, parts.repo);
if (versions.length > 0) {
let sorted = semver.sort(versions);
let latest = sorted[sorted.length - 1];
if (latest.length > 0 && latest[0] == 'v') {
latest = latest.substr(1, latest.length - 1);
}
let latest = semver.strip_v_prefix(sorted[sorted.length - 1]);

// Find wanted (highest satisfying constraint)
let parsed_constraint = semver.parse_constraint(constraint);
let wanted = semver.max_satisfying(versions, parsed_constraint);
if (wanted != null && wanted.length > 0 && wanted[0] == 'v') {
wanted = wanted.substr(1, wanted.length - 1);
}
let wanted = semver.strip_v_prefix(semver.max_satisfying(versions, parsed_constraint));
if (wanted == null) {
wanted = current;
}
Expand Down Expand Up @@ -654,54 +644,14 @@ fn pad_right(s: string, width: i32): string {

// hpm run
fn cmd_run() {
let project_dir = get_project_dir();
let pos_args = get_positional_args();

if (pos_args.length == 0) {
print("Usage: hpm run <script>");
env.exit(EXIT_SUCCESS);
}

let script_name = pos_args[0];

// Read manifest
let pkg = manifest.read_manifest(project_dir);
if (pkg == null) {
print("No package.json found.");
env.exit(EXIT_INVALID_MANIFEST);
}

if (pkg.scripts == null || pkg.scripts[script_name] == null) {
print("Script not found: " + script_name);
env.exit(EXIT_NOT_FOUND);
}

let script_cmd = pkg.scripts[script_name];
print("> " + script_cmd);
print("");

// Execute the script
// Parse command and arguments
let parts = script_cmd.split(" ");
let cmd = parts[0];
let cmd_args = [];
let i = 1;
while (i < parts.length) {
cmd_args.push(parts[i]);
i = i + 1;
}

// Fork and exec
let pid = fork();
if (pid == 0) {
// Child process
exec(cmd, cmd_args);
env.exit(1); // exec failed
} else {
// Parent - wait for child
let status = waitpid(pid);
env.exit(status);
}
cmd_run_with_script(pos_args[0]);
}

// hpm test (shorthand for hpm run test)
Expand Down
40 changes: 38 additions & 2 deletions src/manifest.hml
Original file line number Diff line number Diff line change
Expand Up @@ -224,15 +224,51 @@ export fn parse_specifier(spec: string) {
};
}

// Validate that a name component (owner or repo) contains only safe characters.
// Prevents path traversal and command injection via malicious package names.
export fn is_valid_name_component(s: string): bool {
if (s == null || s.length == 0) {
return false;
}

// Reject path traversal attempts
if (s == "." || s == "..") {
return false;
}

let i = 0;
while (i < s.length) {
let ch = s[i];
let ok = (ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
ch == '-' || ch == '_' || ch == '.';
if (!ok) {
return false;
}
i = i + 1;
}

return true;
}

// Split package name into owner and repo
export fn split_name(name: string) {
let slash_pos = name.find("/");
if (slash_pos < 0) {
return null;
}

let owner = name.substr(0, slash_pos);
let repo = name.substr(slash_pos + 1, name.length - slash_pos - 1);

// Validate components to prevent path traversal
if (!is_valid_name_component(owner) || !is_valid_name_component(repo)) {
return null;
}

return {
owner: name.substr(0, slash_pos),
repo: name.substr(slash_pos + 1, name.length - slash_pos - 1)
owner: owner,
repo: repo
};
}
5 changes: 1 addition & 4 deletions src/resolver.hml
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,7 @@ fn resolve_version(pkg_name: string, pkg_constraints: array) {
let tarball_url = github.get_tarball_url(parts.owner, parts.repo, best_version);

// Remove 'v' prefix for clean version string
let clean_version = best_version;
if (clean_version.length > 0 && clean_version[0] == 'v') {
clean_version = clean_version.substr(1, clean_version.length - 1);
}
let clean_version = semver.strip_v_prefix(best_version);

return {
version: clean_version,
Expand Down
8 changes: 8 additions & 0 deletions src/semver.hml
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,14 @@ export fn sort(versions: array): array {
return result;
}

// Strip leading 'v' or 'V' prefix from a version string
export fn strip_v_prefix(s: string): string {
if (s != null && s.length > 0 && (s[0] == 'v' || s[0] == 'V')) {
return s.substr(1, s.length - 1);
}
return s;
}

// Check if two constraints have any overlap
export fn constraints_overlap(c1, c2): bool {
// This is a simplified check - would need more sophisticated analysis
Expand Down
Loading
Loading