Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,220 @@
|
||||
//! 真实签名测试包通过 production prepare 编译与执行,并验证源码移除后的行为。
|
||||
|
||||
use super::*;
|
||||
use crate::compiler::{build, project::Project};
|
||||
use std::{fs, path::Path, process::Command};
|
||||
#[path = "build_fixture.rs"]
|
||||
mod fixture;
|
||||
|
||||
struct EmptyStore;
|
||||
impl Store for EmptyStore {
|
||||
fn get(&self, _: &str, _: u64) -> Result<Option<Vec<u8>>, String> {
|
||||
Ok(None)
|
||||
}
|
||||
fn put(&self, _: &[u8]) -> Result<(), String> {
|
||||
panic!("Worker prepare must not write cache")
|
||||
}
|
||||
}
|
||||
|
||||
fn project(root: &Path) {
|
||||
for (path, source) in [
|
||||
("app", "value() (value: Int) { value = port.native() }\n"),
|
||||
("port", "native() (value: Int) fails dever.api.Error\n"),
|
||||
("adapter", "external go \"worker/main.go\" {}\n"),
|
||||
("api", "cmd value = app.value\n"),
|
||||
] {
|
||||
fixture::write(root, &format!("module/demo/value/{path}.dever"), source);
|
||||
}
|
||||
fixture::write(
|
||||
root,
|
||||
"module/demo/value/worker/main.go",
|
||||
"package main\nimport (\"context\"; _ \"embed\"; \"dever-worker/helper\")\n//go:embed note.txt\nvar note string\nfunc Native(_ context.Context, _ any, _ any) (any, error) { return map[string]any{\"value\":helper.Value()+Target()+int64(len(note))}, nil }\n",
|
||||
);
|
||||
fixture::write(
|
||||
root,
|
||||
"module/demo/value/worker/helper/value.go",
|
||||
"package helper\nimport \"dever-worker/helper/internal/value\"\nfunc Value() int64 { return value.Get() }\n",
|
||||
);
|
||||
fixture::write(
|
||||
root,
|
||||
"module/demo/value/worker/helper/internal/value/value.go",
|
||||
"package value\nfunc Get() int64 { return 40 }\n",
|
||||
);
|
||||
fixture::write(
|
||||
root,
|
||||
"module/demo/value/worker/target_linux.go",
|
||||
"//go:build linux\npackage main\nfunc Target() int64 { return 1 }\n",
|
||||
);
|
||||
fixture::write(
|
||||
root,
|
||||
"module/demo/value/worker/target_windows.go",
|
||||
"//go:build windows\npackage main\nfunc Target() int64 { return 99 }\n",
|
||||
);
|
||||
fixture::write(root, "module/demo/value/worker/note.txt", "x");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "explicit dev-Pod Go authoring fixture; no product tool fallback"]
|
||||
fn signed_go_pack_compiles_reproducibly_and_runs_native_after_source_removal() {
|
||||
let fixture = fixture::Fixture::new();
|
||||
let root = fixture.root.join("project");
|
||||
project(&root);
|
||||
let project = Project::load(&root).unwrap();
|
||||
let program = project.check().unwrap();
|
||||
let external = program.external_workers().next().unwrap();
|
||||
let target = BuildTarget::host().unwrap();
|
||||
let metadata = library::runtime::metadata(&fixture.signed, "go", target).unwrap();
|
||||
let mut worker = Worker::from_external(external).unwrap();
|
||||
worker.runtime = Some(metadata.pack.clone());
|
||||
let mut lock = Lock::new(vec![]);
|
||||
lock.workers.push(worker);
|
||||
fixture::write(&root, "dever.lock", lock.encode().unwrap());
|
||||
let prepare = || {
|
||||
crate::compiler::project::dependency::worker::prepare_with_store(
|
||||
&root,
|
||||
&program,
|
||||
target,
|
||||
&fixture.signed,
|
||||
&EmptyStore,
|
||||
)
|
||||
};
|
||||
let files = prepare().unwrap();
|
||||
let repeated = prepare().unwrap();
|
||||
assert_eq!(
|
||||
files
|
||||
.iter()
|
||||
.map(|file| (&file.path, &file.bytes, file.executable))
|
||||
.collect::<Vec<_>>(),
|
||||
repeated
|
||||
.iter()
|
||||
.map(|file| (&file.path, &file.bytes, file.executable))
|
||||
.collect::<Vec<_>>()
|
||||
);
|
||||
assert!(!files.iter().any(|file| file.path.contains("/source/")
|
||||
|| file.path.contains("/stdlib/")
|
||||
|| file.path.ends_with("component.go")));
|
||||
let binary = build::build_with_resources(
|
||||
&program,
|
||||
&project.sources,
|
||||
"main",
|
||||
&root.join("app"),
|
||||
target,
|
||||
&files,
|
||||
)
|
||||
.unwrap();
|
||||
// 工具与 SDK 篡改、闭包缺失和目标被排除都必须在交付 Worker 前失败。
|
||||
let installation = fixture.root.join("installation");
|
||||
let release = installation.join("versions/0.0.1");
|
||||
let signature = release.join("manifest.sig");
|
||||
let original = fs::read(&signature).unwrap();
|
||||
fs::write(&signature, "00".repeat(64)).unwrap();
|
||||
assert!(SignedResources::load(&installation, "0.0.1").is_err());
|
||||
fs::write(&signature, original).unwrap();
|
||||
for relative in [
|
||||
"sdk/go/component.go".into(),
|
||||
format!("runtime/go/{}/runtime.pack", target.name()),
|
||||
format!("runtime/go/{}/manifest.json", target.name()),
|
||||
] {
|
||||
let path = release.join(relative);
|
||||
let original = fs::read(&path).unwrap();
|
||||
let mut changed = original.clone();
|
||||
let middle = changed.len() / 2;
|
||||
changed[middle] ^= 1;
|
||||
fs::write(&path, changed).unwrap();
|
||||
assert!(
|
||||
prepare()
|
||||
.err()
|
||||
.expect("tampered signed asset was accepted")
|
||||
.contains("signed resource bytes differ")
|
||||
);
|
||||
fs::write(&path, original).unwrap();
|
||||
let saved = fixture.root.join("missing-asset");
|
||||
fs::rename(&path, &saved).unwrap();
|
||||
assert!(prepare().is_err(), "missing asset used a host fallback");
|
||||
fs::rename(saved, path).unwrap();
|
||||
}
|
||||
let main = root.join("module/demo/value/worker/main.go");
|
||||
let source = fs::read_to_string(&main).unwrap();
|
||||
fs::write(
|
||||
&main,
|
||||
source.replace("dever-worker/helper", "example.com/unlocked"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
prepare()
|
||||
.err()
|
||||
.expect("unlocked import was accepted")
|
||||
.contains("locked module graph")
|
||||
);
|
||||
fs::write(&main, format!("//go:build windows\n{source}")).unwrap();
|
||||
assert!(
|
||||
prepare()
|
||||
.err()
|
||||
.expect("excluded entry was accepted")
|
||||
.contains("excluded by target")
|
||||
);
|
||||
fs::write(&main, &source).unwrap();
|
||||
for import in ["internal/abi", "dever-worker/helper/internal/value"] {
|
||||
fs::write(
|
||||
&main,
|
||||
source.replace("_ \"embed\"", &format!("_ \"embed\"; _ {import:?}")),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
prepare()
|
||||
.err()
|
||||
.expect("private import was accepted")
|
||||
.contains("Go internal import")
|
||||
);
|
||||
}
|
||||
fs::write(&main, &source).unwrap();
|
||||
fixture::write(
|
||||
&root,
|
||||
"module/demo/value/worker/cgo.go",
|
||||
"package main\nimport \"C\"\n",
|
||||
);
|
||||
assert!(prepare().is_err());
|
||||
fs::remove_file(root.join("module/demo/value/worker/cgo.go")).unwrap();
|
||||
let descriptor: Descriptor = serde_json::from_value(fixture.descriptor.clone()).unwrap();
|
||||
let mut missing = fixture.files.clone();
|
||||
missing.remove("stdlib/runtime.a");
|
||||
assert!(descriptor.validate(&missing, target).is_err());
|
||||
let mut missing = fixture.files.clone();
|
||||
missing.remove("bin/compile");
|
||||
assert!(descriptor.validate(&missing, target).is_err());
|
||||
let mut version = descriptor.clone();
|
||||
let actual = version.go_version.split_whitespace().nth(2).unwrap();
|
||||
version.go_version = version.go_version.replace(actual, "go0.0.0");
|
||||
assert!(
|
||||
version
|
||||
.validate(&fixture.files, target)
|
||||
.unwrap_err()
|
||||
.contains("archive target/compiler version differs")
|
||||
);
|
||||
let mut wrong = fixture.descriptor.clone();
|
||||
wrong["goarch"] = "unsupported".into();
|
||||
assert!(
|
||||
serde_json::from_value::<Descriptor>(wrong)
|
||||
.unwrap()
|
||||
.validate(&fixture.files, target)
|
||||
.is_err()
|
||||
);
|
||||
fs::remove_dir_all(root.join("module")).unwrap();
|
||||
fs::remove_dir_all(fixture.root.join("installation")).unwrap();
|
||||
for _ in 0..2 {
|
||||
let output = Command::new(&binary)
|
||||
.env_clear()
|
||||
.args(["demo.value.value", "{}"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
assert_eq!(
|
||||
output.stdout,
|
||||
b"{\"code\":0,\"message\":\"ok\",\"data\":42}\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
//! 只在测试自己的目录中,用 dev Pod 已安装的绝对路径 Go 工具制作签名 fixture。
|
||||
|
||||
use super::*;
|
||||
use ring::signature::{Ed25519KeyPair, KeyPair};
|
||||
use std::{
|
||||
fs,
|
||||
os::unix::fs::{DirBuilderExt, PermissionsExt},
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
pub struct Fixture {
|
||||
pub root: PathBuf,
|
||||
pub signed: SignedResources,
|
||||
pub files: BTreeMap<String, Vec<u8>>,
|
||||
pub descriptor: serde_json::Value,
|
||||
}
|
||||
|
||||
pub fn write(root: &Path, path: &str, bytes: impl AsRef<[u8]>) {
|
||||
let path = root.join(path);
|
||||
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
for directory in path
|
||||
.parent()
|
||||
.unwrap()
|
||||
.ancestors()
|
||||
.take_while(|path| path.starts_with(root))
|
||||
{
|
||||
fs::set_permissions(directory, fs::Permissions::from_mode(0o700)).unwrap();
|
||||
}
|
||||
fs::write(&path, bytes).unwrap();
|
||||
fs::set_permissions(path, fs::Permissions::from_mode(0o600)).unwrap();
|
||||
}
|
||||
|
||||
fn go(root: &Path, arguments: &[&str]) -> String {
|
||||
let result = Command::new("/usr/local/go/bin/go")
|
||||
.args(arguments)
|
||||
.current_dir(root)
|
||||
.env_clear()
|
||||
.env("GOTOOLCHAIN", "local")
|
||||
.env("GOPROXY", "off")
|
||||
.env("GOSUMDB", "off")
|
||||
.env("CGO_ENABLED", "0")
|
||||
.env("GO111MODULE", "off")
|
||||
.env("GOCACHE", root.join("go-cache"))
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
result.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&result.stderr)
|
||||
);
|
||||
String::from_utf8(result.stdout).unwrap()
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
pub fn new() -> Self {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"dever-go-acceptance-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
));
|
||||
fs::DirBuilder::new().mode(0o700).create(&root).unwrap();
|
||||
let version = go(&root, &["version"]);
|
||||
let target = BuildTarget::host().unwrap();
|
||||
let arch = if target == BuildTarget::LinuxX86_64 {
|
||||
"amd64"
|
||||
} else {
|
||||
"arm64"
|
||||
};
|
||||
let version_number = version
|
||||
.split_whitespace()
|
||||
.nth(2)
|
||||
.unwrap()
|
||||
.strip_prefix("go")
|
||||
.unwrap();
|
||||
write(&root, "selector.go", include_bytes!("selector.go"));
|
||||
go(
|
||||
&root,
|
||||
&["build", "-trimpath", "-o", "selector", "selector.go"],
|
||||
);
|
||||
let mut files = BTreeMap::new();
|
||||
files.insert(
|
||||
"bin/analyze".into(),
|
||||
fs::read(root.join("selector")).unwrap(),
|
||||
);
|
||||
for tool in ["compile", "link"] {
|
||||
files.insert(
|
||||
format!("bin/{tool}"),
|
||||
fs::read(format!("/usr/local/go/pkg/tool/linux_{arch}/{tool}")).unwrap(),
|
||||
);
|
||||
}
|
||||
let exports = go(
|
||||
&root,
|
||||
&[
|
||||
"list",
|
||||
"-deps",
|
||||
"-export",
|
||||
"-f",
|
||||
"{{if .Export}}{{.ImportPath}}={{.Export}}{{end}}",
|
||||
"bytes",
|
||||
"context",
|
||||
"encoding/binary",
|
||||
"encoding/json",
|
||||
"errors",
|
||||
"fmt",
|
||||
"io",
|
||||
"math",
|
||||
"os",
|
||||
"regexp",
|
||||
"sort",
|
||||
"strconv",
|
||||
"strings",
|
||||
"time",
|
||||
"unicode/utf8",
|
||||
"embed",
|
||||
],
|
||||
);
|
||||
let mut cfg = String::new();
|
||||
for line in exports.lines().filter(|line| !line.is_empty()) {
|
||||
let (name, source) = line.split_once('=').unwrap();
|
||||
let path = format!("stdlib/{name}.a");
|
||||
cfg.push_str(&format!("packagefile {name}={path}\n"));
|
||||
files.insert(path, fs::read(source).unwrap());
|
||||
}
|
||||
files.insert("stdlib/importcfg".into(), cfg.into_bytes());
|
||||
let descriptor = serde_json::json!({"host":target.name(), "compiler":"bin/compile", "linker":"bin/link", "analyzer":"bin/analyze",
|
||||
"stdlib_importcfg":"stdlib/importcfg", "goos":"linux", "goarch":arch, "go_version":version.trim()});
|
||||
files.insert("dever-runtime.json".into(), serde_json::to_vec(&serde_json::json!({"format":"dever-worker-runtime-v1", "ecosystem":"go", "target":target.name(),
|
||||
"executable":null, "arguments":[], "build":descriptor, "python_markers":null, "python_wheel_tags":[]})).unwrap());
|
||||
let archive = archive(&files);
|
||||
let metadata = serde_json::to_vec(&serde_json::json!({"format":"dever-registry-runtime-v1", "runtime":{"pack":{"name":"go", "version":version_number,
|
||||
"sha256":crate::compiler::project::dependency::digest(&archive)}, "target":target.name(), "python_markers":null, "python_wheel_tags":[]}})).unwrap();
|
||||
let loader = if arch == "amd64" {
|
||||
"ld-linux-x86-64.so.2"
|
||||
} else {
|
||||
"ld-linux-aarch64.so.1"
|
||||
};
|
||||
let loader_source = if arch == "amd64" {
|
||||
"/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2"
|
||||
} else {
|
||||
"/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1"
|
||||
};
|
||||
let artifacts = BTreeMap::from([
|
||||
(
|
||||
format!("runtime/go/{}/runtime.pack", target.name()),
|
||||
archive,
|
||||
),
|
||||
(
|
||||
format!("runtime/go/{}/manifest.json", target.name()),
|
||||
metadata,
|
||||
),
|
||||
(
|
||||
format!("process-runtime/{}/lib/{loader}", target.name()),
|
||||
fs::read(loader_source).unwrap(),
|
||||
),
|
||||
(
|
||||
"sdk/go/component.go".into(),
|
||||
fs::read(Path::new(env!("CARGO_MANIFEST_DIR")).join("../sdk/go/component.go"))
|
||||
.unwrap(),
|
||||
),
|
||||
]);
|
||||
let signed = sign(&root.join("installation"), &artifacts);
|
||||
Self {
|
||||
root,
|
||||
signed,
|
||||
files,
|
||||
descriptor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn archive(files: &BTreeMap<String, Vec<u8>>) -> Vec<u8> {
|
||||
let gzip = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
|
||||
let mut tar = tar::Builder::new(gzip);
|
||||
for (name, bytes) in files {
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_size(bytes.len() as u64);
|
||||
header.set_mode(0o644);
|
||||
header.set_cksum();
|
||||
tar.append_data(&mut header, name, bytes.as_slice())
|
||||
.unwrap();
|
||||
}
|
||||
tar.into_inner().unwrap().finish().unwrap()
|
||||
}
|
||||
|
||||
pub fn sign(root: &Path, artifacts: &BTreeMap<String, Vec<u8>>) -> SignedResources {
|
||||
let key = Ed25519KeyPair::from_seed_unchecked(&[93; 32]).unwrap();
|
||||
let hex = |bytes: &[u8]| {
|
||||
bytes
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect::<String>()
|
||||
};
|
||||
write(
|
||||
root,
|
||||
"state/trusted-release-key",
|
||||
hex(key.public_key().as_ref()),
|
||||
);
|
||||
let entries = artifacts.iter().map(|(path, bytes)| {
|
||||
write(root, &format!("versions/0.0.1/{path}"), bytes);
|
||||
serde_json::json!({"path":path, "bytes":bytes.len(), "sha256":crate::compiler::project::dependency::digest(bytes)})
|
||||
}).collect::<Vec<_>>();
|
||||
let manifest = serde_json::to_vec(&serde_json::json!({"format":"dever-release-v2", "version":"0.0.1", "platform":BuildTarget::host().unwrap().name(), "artifacts":entries, "extensions":[]})).unwrap();
|
||||
write(root, "versions/0.0.1/manifest.json", &manifest);
|
||||
write(
|
||||
root,
|
||||
"versions/0.0.1/manifest.sig",
|
||||
hex(key.sign(&manifest).as_ref()),
|
||||
);
|
||||
SignedResources::load(root, "0.0.1").unwrap()
|
||||
}
|
||||
|
||||
impl Drop for Fixture {
|
||||
fn drop(&mut self) {
|
||||
fs::remove_dir_all(&self.root).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
//! 测试 key 只用于显式注入的 sumdb 对端,不进入产品公钥或项目设置。
|
||||
|
||||
use super::*;
|
||||
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||
use ring::signature::{Ed25519KeyPair, KeyPair};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
io::{Cursor, Write},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct Cache(pub RefCell<BTreeMap<String, Vec<u8>>>);
|
||||
impl Store for Cache {
|
||||
fn get(&self, hash: &str, _: u64) -> Result<Option<Vec<u8>>, String> {
|
||||
Ok(self.0.borrow().get(hash).cloned())
|
||||
}
|
||||
fn put(&self, bytes: &[u8]) -> Result<(), String> {
|
||||
self.0.borrow_mut().insert(
|
||||
crate::compiler::project::dependency::digest(bytes),
|
||||
bytes.to_vec(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(super) struct Registry(pub BTreeMap<String, Vec<u8>>);
|
||||
impl Transport for Registry {
|
||||
fn get(&self, url: &str, limit: usize) -> Result<Vec<u8>, String> {
|
||||
let bytes = self
|
||||
.0
|
||||
.get(url)
|
||||
.ok_or_else(|| format!("missing owned registry response {url}"))?;
|
||||
assert!(bytes.len() <= limit);
|
||||
Ok(bytes.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn runtime() -> Metadata {
|
||||
serde_json::from_value(serde_json::json!({"pack":{"name":"go", "version":"1.25.0", "sha256":"0".repeat(64)},
|
||||
"target":BuildTarget::host().unwrap().name(), "python_markers":null, "python_wheel_tags":[]})).unwrap()
|
||||
}
|
||||
|
||||
pub(super) fn zip(module: &str, version: &str, contents: &str) -> Vec<u8> {
|
||||
let mut archive = zip::ZipWriter::new(Cursor::new(Vec::new()));
|
||||
archive
|
||||
.start_file(
|
||||
format!("{module}@v{version}/value.go"),
|
||||
zip::write::SimpleFileOptions::default(),
|
||||
)
|
||||
.unwrap();
|
||||
archive.write_all(contents.as_bytes()).unwrap();
|
||||
archive.finish().unwrap().into_inner()
|
||||
}
|
||||
|
||||
fn key() -> Ed25519KeyPair {
|
||||
Ed25519KeyPair::from_seed_unchecked(&[47; 32]).unwrap()
|
||||
}
|
||||
fn key_id() -> Vec<u8> {
|
||||
let mut hash = Sha256::new();
|
||||
hash.update(b"dever-sumdb-test\n");
|
||||
hash.update([1]);
|
||||
hash.update(key().public_key().as_ref());
|
||||
hash.finalize()[..4].to_vec()
|
||||
}
|
||||
pub(super) fn verifier() -> Verifier {
|
||||
let mut bytes = vec![1];
|
||||
bytes.extend_from_slice(key().public_key().as_ref());
|
||||
Verifier::new(&format!(
|
||||
"dever-sumdb-test+{}+{}",
|
||||
key_id()
|
||||
.iter()
|
||||
.map(|byte| format!("{byte:02x}"))
|
||||
.collect::<String>(),
|
||||
STANDARD.encode(bytes)
|
||||
))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn root(hashes: &[[u8; 32]]) -> [u8; 32] {
|
||||
if hashes.is_empty() {
|
||||
return Sha256::digest([]).into();
|
||||
}
|
||||
if hashes.len() == 1 {
|
||||
return hashes[0];
|
||||
}
|
||||
let split = hashes.len().next_power_of_two() / 2;
|
||||
let mut hash = Sha256::new();
|
||||
hash.update([1]);
|
||||
hash.update(root(&hashes[..split]));
|
||||
hash.update(root(&hashes[split..]));
|
||||
hash.finalize().into()
|
||||
}
|
||||
|
||||
impl Registry {
|
||||
pub fn module(&mut self, name: &str, version: &str, source: &str) {
|
||||
let prefix = format!(
|
||||
"https://proxy.golang.org/{}/@v/v{version}",
|
||||
module::escape(name)
|
||||
);
|
||||
self.0
|
||||
.insert(format!("{prefix}.mod"), source.as_bytes().to_vec());
|
||||
self.0.insert(
|
||||
format!("{prefix}.zip"),
|
||||
zip(name, version, "package value\n"),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn seal(&mut self) {
|
||||
self.0
|
||||
.retain(|path, _| !path.starts_with("https://sum.golang.org/"));
|
||||
let records = self
|
||||
.0
|
||||
.iter()
|
||||
.filter_map(|(path, source)| {
|
||||
let path = path.strip_prefix("https://proxy.golang.org/")?;
|
||||
let (name, version) = path.split_once("/@v/v")?;
|
||||
let version = version.strip_suffix(".mod")?;
|
||||
let archive =
|
||||
&self.0[&format!("https://proxy.golang.org/{name}/@v/v{version}.zip")];
|
||||
let text = format!(
|
||||
"{name} v{version} {}\n{name} v{version}/go.mod {}\n",
|
||||
sumdb::zip_hash(name, version, archive).unwrap(),
|
||||
sumdb::mod_hash(source)
|
||||
);
|
||||
Some((
|
||||
format!("https://sum.golang.org/lookup/{name}@v{version}"),
|
||||
text,
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let hashes = records
|
||||
.iter()
|
||||
.map(|(_, text)| {
|
||||
let mut hash = Sha256::new();
|
||||
hash.update([0]);
|
||||
hash.update(text);
|
||||
hash.finalize().into()
|
||||
})
|
||||
.collect::<Vec<[u8; 32]>>();
|
||||
let text = format!(
|
||||
"go.sum database tree\n{}\n{}\n",
|
||||
hashes.len(),
|
||||
STANDARD.encode(root(&hashes))
|
||||
);
|
||||
let mut signature = key_id();
|
||||
signature.extend_from_slice(key().sign(text.as_bytes()).as_ref());
|
||||
let note = format!(
|
||||
"{text}\n— dever-sumdb-test {}\n",
|
||||
STANDARD.encode(signature)
|
||||
);
|
||||
self.0.insert(
|
||||
"https://sum.golang.org/latest".into(),
|
||||
note.as_bytes().to_vec(),
|
||||
);
|
||||
for (id, (path, text)) in records.into_iter().enumerate() {
|
||||
self.0
|
||||
.insert(path, format!("{id}\n{text}\n{note}").into_bytes());
|
||||
}
|
||||
assert!(hashes.len() < 256);
|
||||
self.0.insert(
|
||||
format!("https://sum.golang.org/tile/8/0/000.p/{}", hashes.len()),
|
||||
hashes.into_iter().flatten().collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn graph() -> Registry {
|
||||
let mut registry = Registry::default();
|
||||
for (name, version, source) in [
|
||||
(
|
||||
"a",
|
||||
"1.0.0",
|
||||
"module example.com/a\nrequire (\nexample.com/b v1.0.0\nexample.com/c v1.0.0\n)\n",
|
||||
),
|
||||
("b", "1.0.0", "module example.com/b\n"),
|
||||
("b", "1.2.0", "module example.com/b\n"),
|
||||
(
|
||||
"c",
|
||||
"1.0.0",
|
||||
"module example.com/c\nrequire example.com/b v1.2.0\n",
|
||||
),
|
||||
] {
|
||||
registry.module(&format!("example.com/{name}"), version, source);
|
||||
}
|
||||
registry.seal();
|
||||
registry
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
//! Go 模块协议验收只使用拥有的传输和临时摘要缓存。
|
||||
|
||||
use super::*;
|
||||
mod fixture;
|
||||
mod proofs;
|
||||
|
||||
#[test]
|
||||
fn independent_worker_graphs_merge_without_erasing_previous_modules() {
|
||||
let mut registry = fixture::graph();
|
||||
registry.module(
|
||||
"example.com/z",
|
||||
"1.0.0",
|
||||
"module example.com/z\nrequire example.com/b v1.0.0\n",
|
||||
);
|
||||
registry.seal();
|
||||
let cache = fixture::Cache::default();
|
||||
let mut lock = Lock::new(vec![]);
|
||||
for root in ["go:example.com/a@1.0.0", "go:example.com/z@1.0.0"] {
|
||||
resolve_with(
|
||||
&[root.parse().unwrap()],
|
||||
BuildTarget::host().unwrap(),
|
||||
&fixture::runtime(),
|
||||
®istry,
|
||||
&cache,
|
||||
&mut lock,
|
||||
&fixture::verifier(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
assert_eq!(lock.libs.len(), 5);
|
||||
assert_eq!(
|
||||
lock.libs
|
||||
.iter()
|
||||
.filter(|lib| lib.spec.name == "example.com/b")
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
validate_with(&lock, &fixture::verifier()).unwrap();
|
||||
for (name, version) in [("a", "1.2.0"), ("z", "1.0.0")] {
|
||||
let closure = selected(
|
||||
&lock.libs,
|
||||
&[format!("go:example.com/{name}@1.0.0").parse().unwrap()],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
closure
|
||||
.iter()
|
||||
.find(|lib| lib.spec.name == "example.com/b")
|
||||
.unwrap()
|
||||
.spec
|
||||
.version,
|
||||
version
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_authenticated_mvs_graph_and_replays_locked_hashes() {
|
||||
let registry = fixture::graph();
|
||||
let cache = fixture::Cache::default();
|
||||
let mut lock = Lock::new(vec![]);
|
||||
let runtime = fixture::runtime();
|
||||
resolve_with(
|
||||
&["go:example.com/a@1.0.0".parse().unwrap()],
|
||||
BuildTarget::host().unwrap(),
|
||||
&runtime,
|
||||
®istry,
|
||||
&cache,
|
||||
&mut lock,
|
||||
&fixture::verifier(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(lock.libs.len(), 4);
|
||||
assert_eq!(
|
||||
selected(&lock.libs, &["go:example.com/a@1.0.0".parse().unwrap()])
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.find(|lib| lib.spec.name == "example.com/b")
|
||||
.unwrap()
|
||||
.spec
|
||||
.version,
|
||||
"1.2.0"
|
||||
);
|
||||
let records = evidence(&lock).unwrap();
|
||||
assert!(
|
||||
records[0]
|
||||
.records
|
||||
.iter()
|
||||
.any(|record| record.module == "example.com/b" && record.version == "1.0.0")
|
||||
);
|
||||
let checked = validate_with(&lock, &fixture::verifier()).unwrap();
|
||||
for lib in &lock.libs {
|
||||
let artifact = &lib.artifacts[0];
|
||||
let bytes = cache
|
||||
.get(&artifact.sha256, artifact.bytes)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
checked
|
||||
.verify_zip(&lib.spec.name, &lib.spec.version, &bytes)
|
||||
.unwrap();
|
||||
assert!(
|
||||
checked
|
||||
.verify_zip(&lib.spec.name, &lib.spec.version, &bytes[..bytes.len() / 2])
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
validate_lock(&lock).is_err(),
|
||||
"fixture signing key must never become a product trust root"
|
||||
);
|
||||
lock.libs[0].dependencies.clear();
|
||||
assert!(validate_with(&lock, &fixture::verifier()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mvs_keeps_requirements_from_unselected_versions() {
|
||||
let mut registry = fixture::Registry::default();
|
||||
for (name, version, requires) in [
|
||||
(
|
||||
"a",
|
||||
"1.0.0",
|
||||
"require example.com/b v1.0.0\nrequire example.com/c v1.0.0\n",
|
||||
),
|
||||
("b", "1.0.0", "require example.com/d v1.2.0\n"),
|
||||
("b", "1.1.0", "require example.com/d v1.0.0\n"),
|
||||
(
|
||||
"c",
|
||||
"1.0.0",
|
||||
"require example.com/b v1.1.0\nrequire example.com/d v1.0.0\n",
|
||||
),
|
||||
("d", "1.0.0", ""),
|
||||
("d", "1.2.0", ""),
|
||||
] {
|
||||
registry.module(
|
||||
&format!("example.com/{name}"),
|
||||
version,
|
||||
&format!("module example.com/{name}\ngo 1.16\n{requires}"),
|
||||
);
|
||||
}
|
||||
registry.seal();
|
||||
let roots = ["go:example.com/a@1.0.0".parse().unwrap()];
|
||||
let mut lock = Lock::new(vec![]);
|
||||
resolve_with(
|
||||
&roots,
|
||||
BuildTarget::host().unwrap(),
|
||||
&fixture::runtime(),
|
||||
®istry,
|
||||
&fixture::Cache::default(),
|
||||
&mut lock,
|
||||
&fixture::verifier(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(lock.libs.len(), 6);
|
||||
let versions = selected(&lock.libs, &roots)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|lib| (lib.spec.name.as_str(), lib.spec.version.as_str()))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
assert_eq!(versions["example.com/b"], "1.1.0");
|
||||
assert_eq!(versions["example.com/d"], "1.2.0");
|
||||
let older = lock
|
||||
.libs
|
||||
.iter_mut()
|
||||
.find(|lib| lib.spec.name == "example.com/b" && lib.spec.version == "1.0.0")
|
||||
.unwrap();
|
||||
older.dependencies[0].spec.version = "1.0.0".into();
|
||||
assert!(validate_with(&lock, &fixture::verifier()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_module_keeps_distinct_worker_selections() {
|
||||
let mut registry = fixture::Registry::default();
|
||||
for (name, version, requires) in [
|
||||
("x", "1.0.0", "require example.com/d v1.0.0\n"),
|
||||
("y", "1.0.0", "require example.com/d v1.1.0\n"),
|
||||
("d", "1.0.0", ""),
|
||||
("d", "1.1.0", ""),
|
||||
] {
|
||||
registry.module(
|
||||
&format!("example.com/{name}"),
|
||||
version,
|
||||
&format!("module example.com/{name}\n{requires}"),
|
||||
);
|
||||
}
|
||||
registry.seal();
|
||||
let first = vec!["go:example.com/x@1.0.0".parse().unwrap()];
|
||||
let second = vec![
|
||||
"go:example.com/x@1.0.0".parse().unwrap(),
|
||||
"go:example.com/y@1.0.0".parse().unwrap(),
|
||||
];
|
||||
let mut lock = Lock::new(vec![]);
|
||||
for roots in [&first, &second] {
|
||||
resolve_with(
|
||||
roots,
|
||||
BuildTarget::host().unwrap(),
|
||||
&fixture::runtime(),
|
||||
®istry,
|
||||
&fixture::Cache::default(),
|
||||
&mut lock,
|
||||
&fixture::verifier(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let restored: Lock = serde_json::from_str(&lock.encode().unwrap()).unwrap();
|
||||
validate_with(&restored, &fixture::verifier()).unwrap();
|
||||
assert_eq!(restored.libs.len(), 4);
|
||||
for (roots, expected) in [(&first, "1.0.0"), (&second, "1.1.0")] {
|
||||
let closure = selected(&restored.libs, roots).unwrap();
|
||||
assert_eq!(
|
||||
closure
|
||||
.iter()
|
||||
.filter(|lib| lib.spec.name == "example.com/d")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
closure
|
||||
.iter()
|
||||
.find(|lib| lib.spec.name == "example.com/d")
|
||||
.unwrap()
|
||||
.spec
|
||||
.version,
|
||||
expected
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejected_metadata_archive_or_rollback_never_publishes_lock() {
|
||||
let registry = fixture::graph();
|
||||
let runtime = fixture::runtime();
|
||||
for suffix in ["mod", "zip"] {
|
||||
let mut changed = registry.clone();
|
||||
changed
|
||||
.0
|
||||
.get_mut(&format!(
|
||||
"https://proxy.golang.org/example.com/b/@v/v1.0.0.{suffix}"
|
||||
))
|
||||
.unwrap()
|
||||
.extend(b"changed");
|
||||
if suffix == "zip" {
|
||||
changed.0.insert(
|
||||
"https://proxy.golang.org/example.com/b/@v/v1.2.0.zip".into(),
|
||||
fixture::zip("example.com/b", "1.2.0", "changed"),
|
||||
);
|
||||
}
|
||||
let cache = fixture::Cache::default();
|
||||
let mut lock = Lock::new(vec![]);
|
||||
let before = lock.encode().unwrap();
|
||||
assert!(
|
||||
resolve_with(
|
||||
&["go:example.com/a@1.0.0".parse().unwrap()],
|
||||
BuildTarget::host().unwrap(),
|
||||
&runtime,
|
||||
&changed,
|
||||
&cache,
|
||||
&mut lock,
|
||||
&fixture::verifier()
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(lock.encode().unwrap(), before);
|
||||
assert!(cache.0.borrow().is_empty());
|
||||
}
|
||||
let cache = fixture::Cache::default();
|
||||
let mut lock = Lock::new(vec![]);
|
||||
let mut first = fixture::Registry::default();
|
||||
first.module("example.com/a", "1.0.0", "module example.com/a\n");
|
||||
first.seal();
|
||||
let roots = ["go:example.com/a@1.0.0".parse().unwrap()];
|
||||
resolve_with(
|
||||
&roots,
|
||||
BuildTarget::host().unwrap(),
|
||||
&runtime,
|
||||
&first,
|
||||
&cache,
|
||||
&mut lock,
|
||||
&fixture::verifier(),
|
||||
)
|
||||
.unwrap();
|
||||
let mut next = first.clone();
|
||||
next.module("example.com/z", "1.0.0", "module example.com/z\n");
|
||||
next.seal();
|
||||
resolve_with(
|
||||
&roots,
|
||||
BuildTarget::host().unwrap(),
|
||||
&runtime,
|
||||
&next,
|
||||
&cache,
|
||||
&mut lock,
|
||||
&fixture::verifier(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(lock.go_sumdb.len(), 2);
|
||||
let before = lock.encode().unwrap();
|
||||
let error = resolve_with(
|
||||
&roots,
|
||||
BuildTarget::host().unwrap(),
|
||||
&runtime,
|
||||
&first,
|
||||
&cache,
|
||||
&mut lock,
|
||||
&fixture::verifier(),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(error.contains("rollback"), "{error}");
|
||||
assert_eq!(lock.encode().unwrap(), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn module_directives_check_identity_toolchain_and_requirements() {
|
||||
let source = b"module example.com/a\ngo 1.25\nrequire (\nexample.com/b v1.2.0 // indirect\n)\n";
|
||||
let parsed = module::requirements(source, "example.com/a", "1.25.0").unwrap();
|
||||
assert_eq!(parsed["example.com/b"], "1.2.0");
|
||||
assert!(module::requirements(source, "example.com/other", "1.25.0").is_err());
|
||||
assert!(module::requirements(source, "example.com/a", "1.24.0").is_err());
|
||||
for directive in [
|
||||
"replace example.com/b => ../local",
|
||||
"exclude example.com/b v1.0.0",
|
||||
"require (\nexample.com/b v1.0.0",
|
||||
"module example.com/a",
|
||||
] {
|
||||
assert!(
|
||||
module::requirements(
|
||||
format!("module example.com/a\n{directive}\n").as_bytes(),
|
||||
"example.com/a",
|
||||
"1.25.0"
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
assert_eq!(module::escape("example.com/Mixed"), "example.com/!mixed");
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//! 中间归档只检查文件属性;读取计数与异常文件覆盖不启动 Go 工具。
|
||||
|
||||
use super::*;
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
fn stage() -> Stage {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"dever-go-output-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos()
|
||||
));
|
||||
fs::DirBuilder::new().mode(0o700).create(&root).unwrap();
|
||||
Stage {
|
||||
root,
|
||||
resources: BTreeMap::new(),
|
||||
written: 0,
|
||||
started: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_intermediate_output_checks_metadata_without_reading_archive() {
|
||||
let stage = stage();
|
||||
let path = stage.root.join("package.a");
|
||||
let length = 8 * 1024 * 1024;
|
||||
fs::File::create(&path).unwrap().set_len(length).unwrap();
|
||||
let read_chars = || {
|
||||
fs::read_to_string("/proc/thread-self/io")
|
||||
.unwrap()
|
||||
.lines()
|
||||
.find_map(|line| line.strip_prefix("rchar: "))
|
||||
.unwrap()
|
||||
.parse::<u64>()
|
||||
.unwrap()
|
||||
};
|
||||
let before = read_chars();
|
||||
drop(stage.output(&path).unwrap());
|
||||
let consumed = read_chars() - before;
|
||||
eprintln!("go intermediate: fixture=sparse-zero bytes={length} rchar={consumed}");
|
||||
assert!(
|
||||
consumed < 4096,
|
||||
"read {consumed} bytes for a metadata check"
|
||||
);
|
||||
assert_eq!(fs::metadata(path).unwrap().len(), length);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn go_output_rejects_links_empty_special_and_oversized_files() {
|
||||
let stage = stage();
|
||||
let path = stage.root.join("output");
|
||||
fs::write(&path, b"archive").unwrap();
|
||||
drop(stage.output(&path).unwrap());
|
||||
let link = stage.root.join("symbolic");
|
||||
symlink(&path, &link).unwrap();
|
||||
assert!(stage.output(&link).is_err());
|
||||
fs::remove_file(link).unwrap();
|
||||
let link = stage.root.join("hard");
|
||||
fs::hard_link(&path, &link).unwrap();
|
||||
assert!(stage.output(&path).is_err());
|
||||
fs::remove_file(link).unwrap();
|
||||
assert!(stage.output(&stage.root).is_err());
|
||||
fs::File::create(&path).unwrap();
|
||||
assert!(stage.output(&path).is_err());
|
||||
fs::File::create(&path)
|
||||
.unwrap()
|
||||
.set_len(128 * 1024 * 1024 + 1)
|
||||
.unwrap();
|
||||
assert!(stage.output(&path).is_err());
|
||||
fs::remove_file(&path).unwrap();
|
||||
assert!(stage.output(&path).is_err());
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//! 官方历史签名向量与目录哈希检查,独立于自签测试对端。
|
||||
|
||||
use super::*;
|
||||
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||
use std::io::{Cursor, Write};
|
||||
|
||||
#[test]
|
||||
fn official_inclusion_and_consistency_vectors_reject_each_tampered_hash() {
|
||||
let evidence: Evidence = serde_json::from_str(include_str!("sumdb_uuid.json")).unwrap();
|
||||
let verified = evidence.verify(&Verifier::official()).unwrap();
|
||||
assert!(verified.contains("github.com/google/uuid", "1.6.0"));
|
||||
assert!(evidence.verify(&fixture::verifier()).is_err());
|
||||
assert_eq!(
|
||||
sumdb::mod_hash(b"module github.com/google/uuid\n"),
|
||||
"h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo="
|
||||
);
|
||||
for index in 0..evidence.records[0].inclusion.len() {
|
||||
let mut changed = evidence.clone();
|
||||
changed.records[0].inclusion[index] = STANDARD.encode([0; 32]);
|
||||
assert!(changed.verify(&Verifier::official()).is_err());
|
||||
}
|
||||
for index in 0..evidence.consistency.len() {
|
||||
let mut changed = evidence.clone();
|
||||
changed.consistency[index] = STANDARD.encode([0; 32]);
|
||||
assert!(changed.verify(&Verifier::official()).is_err());
|
||||
}
|
||||
let mut changed = evidence.clone();
|
||||
changed.records[0].go_mod.push_str("// tampered\n");
|
||||
assert!(changed.verify(&Verifier::official()).is_err());
|
||||
let mut changed = evidence.clone();
|
||||
changed.checkpoint = changed.checkpoint.replace("go.sum", "no.sum");
|
||||
assert!(changed.verify(&Verifier::official()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hashzip_tracks_directory_names_but_not_compression_or_order() {
|
||||
let archive = |reverse: bool, directory: bool| {
|
||||
let mut zip = zip::ZipWriter::new(Cursor::new(Vec::new()));
|
||||
let mut names = vec!["value.go", "LICENSE"];
|
||||
if reverse {
|
||||
names.reverse();
|
||||
}
|
||||
for name in names {
|
||||
zip.start_file(
|
||||
format!("example.com/Mixed@v1.0.0/{name}"),
|
||||
zip::write::SimpleFileOptions::default().compression_method(if reverse {
|
||||
zip::CompressionMethod::Deflated
|
||||
} else {
|
||||
zip::CompressionMethod::Stored
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
zip.write_all(b"same bytes").unwrap();
|
||||
}
|
||||
if directory {
|
||||
zip.add_directory(
|
||||
"example.com/Mixed@v1.0.0/empty/",
|
||||
zip::write::SimpleFileOptions::default(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
zip.finish().unwrap().into_inner()
|
||||
};
|
||||
let first = sumdb::zip_hash("example.com/Mixed", "1.0.0", &archive(false, false)).unwrap();
|
||||
assert_eq!(
|
||||
first,
|
||||
sumdb::zip_hash("example.com/Mixed", "1.0.0", &archive(true, false)).unwrap()
|
||||
);
|
||||
assert_ne!(
|
||||
first,
|
||||
sumdb::zip_hash("example.com/Mixed", "1.0.0", &archive(false, true)).unwrap()
|
||||
);
|
||||
assert!(sumdb::zip_hash("example.com/mixed", "1.0.0", &archive(false, false)).is_err());
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// 测试 pack 的固定目标源码选择器;产品只运行签名 pack 提供的对应工具。
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := selectPackage(); err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
|
||||
}
|
||||
|
||||
func selectPackage() error {
|
||||
if len(os.Args) != 4 { return fmt.Errorf("expected OS architecture directory") }
|
||||
context := build.Default
|
||||
context.GOOS, context.GOARCH = os.Args[1], os.Args[2]
|
||||
context.GOROOT, context.GOPATH = "", ""
|
||||
context.CgoEnabled = true
|
||||
directory := os.Args[3]
|
||||
selected, err := context.ImportDir(directory, 0)
|
||||
if err != nil { return err }
|
||||
if len(selected.CgoFiles)+len(selected.CFiles)+len(selected.CXXFiles)+len(selected.SFiles)+len(selected.SysoFiles) != 0 {
|
||||
return fmt.Errorf("cgo/assembly/native objects are unsupported")
|
||||
}
|
||||
embed := map[string][]string{}
|
||||
for _, pattern := range selected.EmbedPatterns {
|
||||
glob := strings.TrimPrefix(pattern, "all:")
|
||||
if filepath.IsAbs(glob) || strings.Contains(glob, "..") { return fmt.Errorf("invalid embed pattern") }
|
||||
matches, err := filepath.Glob(filepath.Join(directory, glob))
|
||||
if err != nil || len(matches) == 0 { return fmt.Errorf("unmatched embed pattern") }
|
||||
for _, match := range matches {
|
||||
err = filepath.WalkDir(match, func(path string, entry fs.DirEntry, failure error) error {
|
||||
if failure != nil { return failure }
|
||||
if entry.Type()&os.ModeSymlink != 0 { return fmt.Errorf("embed link") }
|
||||
if entry.IsDir() { return nil }
|
||||
relative, err := filepath.Rel(directory, path)
|
||||
if err != nil { return err }
|
||||
embed[pattern] = append(embed[pattern], filepath.ToSlash(relative))
|
||||
return nil
|
||||
})
|
||||
if err != nil { return err }
|
||||
}
|
||||
sort.Strings(embed[pattern])
|
||||
}
|
||||
imports := selected.Imports
|
||||
if imports == nil { imports = []string{} }
|
||||
return json.NewEncoder(os.Stdout).Encode(map[string]any{"name":selected.Name,"files":selected.GoFiles,"imports":imports,"embed":embed})
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"previous": "go.sum database tree\n63512409\nF7eHRPqN/cqPtqti14NKdGGaI1Giw8coWP3KxihFZbg=\n\n— sum.golang.org Az3gri1OBHKT87Xj8UXU8zjoHGy/X+7Nl94f8TZkELVaasolmGApUsWqwVG9vE6hgqYb/ph/w2vdkLIVRH7bMB/FPQ4=\n",
|
||||
"checkpoint": "go.sum database tree\n66321505\nnL/ZK4zIq2TWUhMi/cDJQRq6hXj67qFBbyEqxks3epY=\n\n— sum.golang.org Az3gruvaP9rIy6foXnttmnViaXM55mqUcP66AeP7N5r+tpFo2ll40wDr7sN0guXk6rXp0fL63fe30pchMCTqmbRo+Ak=\n",
|
||||
"consistency": [
|
||||
"3OyjS9XS6r41pxaFWGnW/WSmX7b1ik3nupquDNipdZ8=",
|
||||
"zX5IsB02+IutT35ka/MwY7gFiPSAlihfnt2cu6smUjw=",
|
||||
"Qc0CqO89A+/onGz1u4ZsDoMOfRIgPZpayRGTETOv8kk=",
|
||||
"JhdrRO/y8Iu+MDeUupFG0uM4+odAADMjVH0mOgLOA/c=",
|
||||
"lOgywJvTdrBWol0uphM6ZulvAebwJGNlekm8MIwiwKM=",
|
||||
"FRf0N51Fw+8eSLHEa/e/AsnDway0EkKo4jI0E1Ne7dk=",
|
||||
"Ak0gyC+RS6/ahVBCBuSBDA1wFN9mgq3acIbYm9uJHQ4=",
|
||||
"FFJ8WrKTTQgCjTuAl5CukBdjDaQ7opSdHBpNSolRYWw=",
|
||||
"rdNcd+MUjUxzlX/nlVGeQP5BkkZyTT89sn/yfg0AXQ0=",
|
||||
"gJzOh5y5rP1BILUrFJw+aGjod840PErDnjy4T0BoUE4=",
|
||||
"7I8BPI+ADxeJmu19mQxO+xt6bNhg3Ueq1j2dIfOppwA=",
|
||||
"urhcUF9QOyTRAYRjJjsRHjHdwIc+BxfiQ6a+NuYeC6E=",
|
||||
"p9eGEUHXeKLFnrIPB7VXHHMuwtaBnvHG4CYwgC0wq+w=",
|
||||
"Gcry0ScaNfFmQujPMT95RQ3rbzKDSynQsL546C8CkfU=",
|
||||
"dIpym+JRT/+z9/tlhpuIRyv+uoWGqgND/MHw0f1MtqA=",
|
||||
"cQrKLJIBbPoWDnRnbGfAEMfpsau0h+Aw3aqDOJ5LAVU=",
|
||||
"C46761Ui7jL+lf4vRXBlveoxuFS6tykAd6mMEfNttPE=",
|
||||
"45Kz4HcZ9iZ84ySMHCy21d4vd2MvibvfNMzYm0hKYRs=",
|
||||
"4JZTJ+4VUtLMTEs11jn4GmCxoz56Lm5HDwkYmkmAWlw=",
|
||||
"2hJV4h1jZ2YujjyXUw+7YYCwLXGYZy+ondhRUgxKmXI=",
|
||||
"lrn/kRfgYdYSDxgPdSQ8/GnZw6u/joKu9++AR+Saa5M=",
|
||||
"hnm9+kaHQdhHWrAcyrvY5fbv8QB+GwVjRMFeobtTg8A=",
|
||||
"rPFmKL/vD6Edsk/0DmpvDa/nMOqWgjfpPGRjBUL9yKs=",
|
||||
"2IzbyezmwwGZZPJ276ejAQZiAnar+RcjAUU3MEMtbIA=",
|
||||
"k3kfr9AECmkV+UA3/D6EMqaBDr67aubL5hXGsvJ+Nng=",
|
||||
"/WnQtddEqG03mbcU1XID1iws1nlKHVkJcaREWknCOiE=",
|
||||
"xJ20xXM4QItt5vxFOtolZ5l18p7ej1+RimNkgWmoRFM="
|
||||
],
|
||||
"records": [{
|
||||
"module": "github.com/google/uuid",
|
||||
"version": "1.6.0",
|
||||
"go_mod": "module github.com/google/uuid\n",
|
||||
"lookup": "22152757\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\n\ngo.sum database tree\n66321505\nnL/ZK4zIq2TWUhMi/cDJQRq6hXj67qFBbyEqxks3epY=\n\n— sum.golang.org Az3gruvaP9rIy6foXnttmnViaXM55mqUcP66AeP7N5r+tpFo2ll40wDr7sN0guXk6rXp0fL63fe30pchMCTqmbRo+Ak=\n",
|
||||
"inclusion": [
|
||||
"vYU6vRfhcMyd5/U04n+AYg52mm3BQrjwyN+27saiSnU=",
|
||||
"syWiyNbj6gybIdLKoAFd7xF3qNltS7OhQjtp4y8rCFg=",
|
||||
"Zqpa9zJsOg/sI/mv0UkXO9d7gvPHLLjMAKnZCPzeiNM=",
|
||||
"tGbcXa1AQlYBIm5gM2KDy8JvdAFDizj4lUop18Eehxs=",
|
||||
"/w9FG1Sae/omKxZdOvj719sT1rCow3tlP8VNo0fDQtE=",
|
||||
"dR6Bk+gh/hyJx7bu65HfhQdfuvT/OdVv/G7RUsOc26Y=",
|
||||
"fRC86/2mWWtFWNPz/Vou6f7hdyvRLHFPo9KaOlVOVmc=",
|
||||
"jJS2b3XVP2sst4CS5srpnF34agHpiluU63h6UQyFPDc=",
|
||||
"cOL2+zAXTEOaAiyvmCrL3Ned6soAg08CJJJ2VW3+a18=",
|
||||
"In5NzPUzIAZfV6bnUL11Bkm4Aqoit/ZG7gJ8nxOdB2w=",
|
||||
"r+8IH0m7Ig3BS+uwCVVG1CZT0I/Rro3bbSUVYcSrN2Q=",
|
||||
"wOgTamt00bdokE1Nbv3aHYu2um06V1xdLFEmOBjt0GI=",
|
||||
"21woobbbCvogV/ZXACdAozfVAFjRv5xMID/iLcryHzw=",
|
||||
"5+o2gn9/zVod+MYr4UuoeMzuz68vt/hA5+9H4YZaVME=",
|
||||
"040B+RrLQeFCJhPU69kzoKr4BTqhSwdrnYEVhknZ+I0=",
|
||||
"ioI6UByPBEr2lEZB0dI6pCXgcNss6UkKxm+TYAADujg=",
|
||||
"HOJ0dLK4hnl0tFeu1tmT7TRrNKRIbi/T8KI/2s4DXDs=",
|
||||
"BBcHtB4mS19jHff/LQTNI/+WCVBZ1Up52gNqyoln71U=",
|
||||
"Oz/OlF1Lj7wkqqE15p324ZSX1XHcnLzDSMKNtUx6rwY=",
|
||||
"ofpmTAcvFvNzlwjZnsrMqeG4iIBCP2lt5wIusvZ2gG4=",
|
||||
"IRpVS0aap6n0qb02jyOy6ikkOPxdFIQtWwckaGF2MAQ=",
|
||||
"liRusDUG3TanNZLGkfjb58ppX/6d40M7X2s3wptjxMw=",
|
||||
"ROjUzjXHDN0Yo/I6EeqHhpszFgIs7FCABVeQniliPTk=",
|
||||
"NTjIv8CJqIIGrOHhNQWSP9FV6XEPL6uGA4lmMfNKGJg=",
|
||||
"bwqUQG2wJXGxo1QLA2CFb3chH17xvlqXe6eQy43p0HY=",
|
||||
"bqa+4xGvzGcI/PYne2fXfUvdULgy8o3zQmD13q/gu1Y="
|
||||
],
|
||||
"consistency": []
|
||||
}]
|
||||
}
|
||||
Reference in new issue
Block a user