From f5584f15052c39e4c34c554352cdcae3bc0aa0a3 Mon Sep 17 00:00:00 2001 From: mediocregopher <> Date: Sat, 2 May 2020 13:47:34 -0600 Subject: [PATCH] Create the welcome thread, and a README for it --- type: change description: Create the welcome thread, and a README for it fingerprint: ACfbSiTJmQ04DduNlyf0kNvJgqhGkJC1osSEZ9kdO6+o credentials: - type: pgp_signature pub_key_id: 95C46FA6A41148AC body: iQIzBAABAgAdFiEEJ6tQKp6olvZKJ0lwlcRvpqQRSKwFAl6tzlUACgkQlcRvpqQRSKyTYxAAjfPI881Xu168EJmwi2by9QLcUlcYY+t5DmJaxtGB+WT7W9qcfZ3WOwzST+X4rBvoA8oTPnfI6PE2tuF9RPgRBSxn3JOALRH2VwqoY5fsuTOk5/BO1uukPZdycdDpYZRKpQZKC8kzt3KwYskRR4CoxVroqmAzxEVba4dZTAXprov724cU7QXWXjOtU2iX0JNn/S/yX3L3g1v4sOVbaaUmif4aOLntx+7E2R7v28aBg0HL2uTgSs5nsHLXXfdRcm1CFmGzX8FNAChHkpUg9OdDpd5+mqBf7ymKBWuv0z+I2qe6xTPAshcMm3EWfbUpb1+Bux7UpywwZnz97HvdopFnKaHAfbv99Sfm/OqzgMeLClWv3Iysm1k5PcXobvfs2E9MUfIjG085jTZ0cq0OPqGhODkBOVHyn4Cm71ZMELt9yAkihxKLHjkp3J0WwQv0HbEieA0fE6Czmc481oTd0kGlDWTla/LMd3/vU4Gpx89Y9+2lTV0WaXoAawjJmEXQwqSCiPHYSnfAWgjDTkEAkNMHN8HgMYsVxhtipayvYPiWJFRhL5LVKGNgwUefTfhhhvrx1FBza5sF06XB7vKbb3npvZrfm2faLi1eyFX2xIl7m7dY6C4XYr3CBgEPiBh/NiCaZiOtjxOkzrJ/bsWNGolURMhNt9NAWFms8Nz5bXnRwZQ= account: mediocregopher --- .dehub/config.yml | 18 +- .gitignore | 2 +- README.md | 72 ++++++++ SPEC.md | 195 --------------------- accessctl/access_control.go | 53 ------ accessctl/access_control_test.go | 118 ------------- accessctl/condition.go | 130 -------------- accessctl/condition_test.go | 110 ------------ change_hash.go | 75 --------- cmd/dehub/main.go | 136 --------------- commit.go | 251 --------------------------- commit_test.go | 170 ------------------- config.go | 83 --------- diff.go | 41 ----- fs/build_tree_helper.go | 117 ------------- fs/fs.go | 100 ----------- go.mod | 12 -- go.sum | 82 --------- repo.go | 104 ------------ repo_test.go | 118 ------------- sigcred/credential.go | 25 --- sigcred/pgp.go | 279 ------------------------------- sigcred/pgp_test.go | 66 -------- sigcred/sigcred.go | 5 - sigcred/signifier.go | 50 ------ typeobj/typeobj.go | 158 ----------------- typeobj/typeobj_test.go | 114 ------------- yamlutil/yamlutil.go | 32 ---- yamlutil/yamlutil_test.go | 55 ------ 29 files changed, 85 insertions(+), 2686 deletions(-) create mode 100644 README.md delete mode 100644 SPEC.md delete mode 100644 accessctl/access_control.go delete mode 100644 accessctl/access_control_test.go delete mode 100644 accessctl/condition.go delete mode 100644 accessctl/condition_test.go delete mode 100644 change_hash.go delete mode 100644 cmd/dehub/main.go delete mode 100644 commit.go delete mode 100644 commit_test.go delete mode 100644 config.go delete mode 100644 diff.go delete mode 100644 fs/build_tree_helper.go delete mode 100644 fs/fs.go delete mode 100644 go.mod delete mode 100644 go.sum delete mode 100644 repo.go delete mode 100644 repo_test.go delete mode 100644 sigcred/credential.go delete mode 100644 sigcred/pgp.go delete mode 100644 sigcred/pgp_test.go delete mode 100644 sigcred/sigcred.go delete mode 100644 sigcred/signifier.go delete mode 100644 typeobj/typeobj.go delete mode 100644 typeobj/typeobj_test.go delete mode 100644 yamlutil/yamlutil.go delete mode 100644 yamlutil/yamlutil_test.go diff --git a/.dehub/config.yml b/.dehub/config.yml index d844302..d816ad2 100644 --- a/.dehub/config.yml +++ b/.dehub/config.yml @@ -6,9 +6,15 @@ accounts: path: ".dehub/mediocregopher.asc" access_controls: - - pattern: "**" - condition: - type: signature - account_ids: - - mediocregopher - count: 100% + - action: allow + filters: + - type: branch + pattern: public/welcome + - type: payload_type + payload_type: comment + - type: not + filter: + type: commit_attributes + non_fast_forward: true + - type: signature + any: true diff --git a/.gitignore b/.gitignore index bb70765..ef96859 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -dehub +/dehub diff --git a/README.md b/README.md new file mode 100644 index 0000000..97c6066 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# Welcome! + +Hello! Welcome to the dehub project. You've found your way onto the welcome +branch. This branch is open for anyone to leave a comment commit on it, provided +they sign their commit with a PGP key. + +## Viewing comments + +If you've gotten this far then viewing comments is as easy as doing `git log`. +All commits will be shown from newest to oldest. You will only see the latest +snapshot of comments that you've pulled from the server. In order to update that +snapshot do: + +``` +git pull -f origin public/welcome +``` + +## Leaving a comment + +The first step to leaving a comment of your own is to install dehub. Visit +`https://dehub.dev` for more on how to do that. + +Once done, and assuming you have this branch checked out (how are you reading +this if you don't?), just do the following: + +``` +dehub commit --anon-pgp-key=KEY_NAME comment +``` + +(`KEY_NAME` should be replaced with any selector which will match your pgp key, +such as the key ID, the name on the key, or the email.) + +Your default text editor (defined by the `EDITOR` environment variable) will pop +up and you can then write down your comment. When you save and close your editor +dehub will sign the comment with your pgp key and create a commit with it. + +You can view your newly created commit by calling `git show`. + +If after you've created your comment commit (but before you've pushed it) you'd +like to amend it, do: + +``` +dehub commit --anon-pgp-key=KEY_NAME comment --amend +``` + +Finally, to push your comment commit up, you can do: + +``` +git push origin public/welcome +``` + +Once pushed, everyone will be able to see your comment! + +### What to say? + +Here's some starting points if you're not sure what to write in your first +comment: + +* Introduce yourself; say where you're from and what your interests are. +* How did you find dehub? Why is it interesting to you? +* If you're using dehub for a project, shill your project! +* If you'd like to get involved in dehub's development, let us know what your + skills are and how you can help. Remember, it takes more than expert + programmers to make a project successful. + +## Rules + +Please be kind to others, and keep discussion related to dehub and +dehub-adjacent topics. Politics, in general, is not going to be related to +dehub. Comments which are off-topic or otherwise abusive are subject to being +removed. + diff --git a/SPEC.md b/SPEC.md deleted file mode 100644 index 110a0f5..0000000 --- a/SPEC.md +++ /dev/null @@ -1,195 +0,0 @@ -# .dehub - -The `.dehub` directory contains all meta information related to -decentralized repository management and access control. - -## config.yml - -The `.dehub/config.yml` file takes the following structure: - -```yaml -# accounts defines all accounts which are known to the repo. -accounts: - - # Each account is an object with an id and at least one identifier. The id - # must be unique for each account. - - id: some_user_id: - - # signifiers describes different methods the account might use to - # identify itself. Generally, these will be different public keys which - # commits will be signed with. At least one is required. - signifiers: - - type: "pgp_public_key" - body: "FULL PGP PUBLIC KEY STRING" - - - type: "pgp_public_key_file" - path: ".dehub/some_user_id.asc" - - - type: "keybase" - user: "some_keybase_user_id" - -# access_controls defines under what conditions different files in the repo may -# be modified. For each file modified in a commit, all access control patterns -# are applied sequentially until one matches, and the associated access control -# conditions are checked. A commit is only allowed if the conditions of all -# modified files are met. -access_controls: - - # pattern is a glob pattern describing what files this access control - # applies to. Single star matches all characters except path separators, - # double star matches everything. - - pattern: ".dehub/**" - - # signature conditions indicate that a commit must be signed by one or - # more accounts to be allowed. - condition: - type: signature - - # account_ids lists all accounts whose signature will count towards - # meeting the condition - account_ids: - - some_user_id - - # count describes how many signatures are required. It can be either a - # contrete integer (e.g. 2, meaning any 2 accounts listed by - # account_ids) or a percent. - count: 100% - - # This catch-all pattern for the rest of the repo requires that changes to - # any files not under `.dehub/` are signed by at least one of the - # defined accounts. - - pattern: "**" - condition: - type: signature - any_account: true # indicates any account defined in accounts is valid - count: 1 -``` - -# Master commit - -All new commits being appended to the HEAD of the `master` branch are subject to -the following requirements: - -* Must conform to all requirements defined by the `access_controls` section of - the `config.yml`, as found in the HEAD. If the commit is the initial commit of - the repo then it instead uses the `config.yml` found in itself. - -* Must not be a merge commit (this may be amended later, but at present it - simplifies implementation). - -* The commit message must conform to the format and semantics defined below. - -## Master Commit Message - -The commit message for a commit being appended to the HEAD of the `master` -branch must conform to the following format: a single line (the message head) -giving a short description of the change, then two newlines, then a body which -is a yaml formatted string: - -```yaml -This is the message head. It will be re-iterated within the yaml body. - -# Now the yaml body begins ---- -message: > - This is the message head. It will be re-iterated within the yaml body. - - The rest of this field is for the message body, which corresponds to the - body of a normal commit message which might give a more long-form - explanation of the commit's changes. - - Since the message is used in generating the signature it's necessary for it - to be encoded here fully formed, even though the message head is then - duplicated. Otherwise the exact bytes of the message would be ambiguous. - This situation is ugly, but not unbearable. - -# See the Commit Signatures section below for how this is computed. The -# change_hash is always recomputed when verifying a commit, but is reproduced in -# the commit message itself for cases of forward compatibility, e.g. if the -algorithm to compute the hash changes. -change_hash: XXX - -# Credentials are the set of credentials which count towards requirements -# specified in the `access_controls` section of the `config.yml` file. -credentials: - - - type: pgp_signature - account_id: some_user_id - pub_key_id: XXX - body: "base-64 signature body" -``` - -## Commit Signatures - -When a commit is being signed by a signifier there is an expected data format -for the data to be signed. The format is a SHA-256 hash of the following pieces -of data concatenated together (the "change_hash"): - -* A uvarint indicating the number of bytes in the commit message. -* The message. -* A uvarint indicating the number of files changed. -* For each file changed in the commit, ordered lexographically-ascending based - on its full relative path within the repo, the following is then written: - * A uvarint indicating the length of the full relative path of the file - within the repo. - * The full relative path of the file within the repo. - * A little-endian uint32 representing the previous file mode of the file (or 0 - if the file is being inserted). - * The 20-byte SHA1 hash of the previous version of the file's contents (or 20 - 0 bytes if the file is being inserted). - * A little-endian uint32 representing the new file mode of the file (or 0 - if the file is being deleted). - * The 20-byte SHA1 hash of the new version of the file's contents (or 20 - 0 bytes if the file is being deleted). - -The raw output from the SHA-256 is then prepended with a `0` byte (for forward -compatibility) and signed, and the result used as the signature body. - -# Merge Requests - -A merge request (MR) may be pushed to the repository as a new branch at any -time. All MR branch names follow the naming convention `DHMR-short-description`. -An MR branch has the following qualities: - -* Meta commits (see sub-section) will only contain a commit message head/body, - but no file changes. - -* The most recent substantial commit (as opposed to meta commits) should always - contain the full commit message head and body. - -## Meta Commits - -Meta commits are those which add information about the changes being requested, -but do not modify the changes themselves. - -### Signature Commits - -Signature commits sign the changes requested in order to count towards their -access control requirements. The message head of these are arbitrary, but the -body must be formatted as such: - -```yaml -# This object matches the one found in the `credentials` section of the master -# commit message. -type: pgp_signature -account_id: some_user_id ``` -pub_key_id: XXX -body: "base-64 signature body" # see Commit Signatures sub-section. -``` - -If a signature commit is added to a MR branch, and a substantial commit is -added after it, then that signature commit will no longer be valid, as it was -only signing the the prior changeset. The signer will need to create and push a -new signature commit, if they agree with the new changes. - -## Merging MRs - -When an MR has accumulated enough meta commits to fulfuill access control -requirements it may be coalesced into a single commit destined for the master -branch. See the Master Commit Message sub-section for details on how commits in -the master branch must be formatted. - -# TODO - -* access control patterns related to who may push to MR branches, and what types - of commits they can push. diff --git a/accessctl/access_control.go b/accessctl/access_control.go deleted file mode 100644 index 7d50428..0000000 --- a/accessctl/access_control.go +++ /dev/null @@ -1,53 +0,0 @@ -package accessctl - -import ( - "fmt" - - "github.com/bmatcuk/doublestar" -) - -// AccessControl represents an access control object being defined in the -// Config. -type AccessControl struct { - Pattern string `yaml:"pattern"` - Condition Condition `yaml:"condition"` -} - -// ErrNoApplicableAccessControls is returned from ApplicableAccessControls when -// a changed path has no applicable AccessControls which match it. -type ErrNoApplicableAccessControls struct { - Path string -} - -func (err ErrNoApplicableAccessControls) Error() string { - return fmt.Sprintf("no AccessControls which apply to changed file %q", err.Path) -} - -// ApplicableAccessControls returns a subset of the given AccessControls which -// are applicable to the given file paths (ie those whose Conditions must be met -// in order for the changes to go through. -func ApplicableAccessControls(accessControls []AccessControl, filesChanged []string) ([]AccessControl, error) { - applicableSet := map[AccessControl]struct{}{} - for _, path := range filesChanged { - var any bool - for _, ac := range accessControls { - if ok, err := doublestar.PathMatch(ac.Pattern, path); err != nil { - return nil, fmt.Errorf("error matching path %q to patterrn %q: %w", - path, ac.Pattern, err) - } else if ok { - applicableSet[ac] = struct{}{} - any = true - break - } - } - if !any { - return nil, ErrNoApplicableAccessControls{Path: path} - } - } - - applicable := make([]AccessControl, 0, len(applicableSet)) - for ac := range applicableSet { - applicable = append(applicable, ac) - } - return applicable, nil -} diff --git a/accessctl/access_control_test.go b/accessctl/access_control_test.go deleted file mode 100644 index 07326ba..0000000 --- a/accessctl/access_control_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package accessctl - -import ( - "errors" - "reflect" - "sort" - "testing" -) - -func TestApplicableAccessControls(t *testing.T) { - tests := []struct { - descr string - patterns, filesChanged []string - exp []string - expErrPath string - }{ - { - descr: "empty input empty output", - }, - { - descr: "empty patterns", - filesChanged: []string{"foo", "bar"}, - expErrPath: "foo", - }, - { - descr: "empty filesChanged", - patterns: []string{"patternA", "patternB"}, - }, - { - descr: "no applicable files", - filesChanged: []string{"foo"}, - patterns: []string{"bar"}, - expErrPath: "foo", - }, - { - descr: "all applicable files", - filesChanged: []string{"foo", "bar"}, - patterns: []string{"**"}, - exp: []string{"**"}, - }, - { - descr: "pattern precedent", - filesChanged: []string{"foo"}, - patterns: []string{"foo", "**"}, - exp: []string{"foo"}, - }, - { - descr: "pattern precedent inv", - filesChanged: []string{"foo"}, - patterns: []string{"**", "foo"}, - exp: []string{"**"}, - }, - { - descr: "individual matches", - filesChanged: []string{"foo", "bar/baz"}, - patterns: []string{"foo", "bar/baz"}, - exp: []string{"foo", "bar/baz"}, - }, - { - descr: "star match dir", - filesChanged: []string{"foo", "bar/baz"}, - patterns: []string{"foo", "bar/*"}, - exp: []string{"foo", "bar/*"}, - }, - { - descr: "star not match dir", - filesChanged: []string{"foo", "bar/baz/biz"}, - patterns: []string{"foo", "bar/*"}, - expErrPath: "bar/baz/biz", - }, - { - descr: "doublestar match dir", - filesChanged: []string{"foo", "bar/bar", "bar/baz/biz"}, - patterns: []string{"foo", "bar/**"}, - exp: []string{"foo", "bar/**"}, - }, - } - - for _, test := range tests { - t.Run(test.descr, func(t *testing.T) { - accessControls := make([]AccessControl, len(test.patterns)) - for i := range test.patterns { - accessControls[i] = AccessControl{Pattern: test.patterns[i]} - } - - out, err := ApplicableAccessControls(accessControls, test.filesChanged) - if err != nil && test.expErrPath == "" { - t.Fatalf("unexpected error: %v", err) - } else if test.expErrPath != "" { - if noAppErr := (ErrNoApplicableAccessControls{}); !errors.As(err, &noAppErr) { - t.Fatalf("expected ErrNoApplicableAccessControls for path %q, but got %v", test.expErrPath, err) - } else if test.expErrPath != noAppErr.Path { - t.Fatalf("expected ErrNoApplicableAccessControls for path %q, but got one for path %q", test.expErrPath, noAppErr.Path) - } - return - } - - outPatterns := make([]string, len(out)) - for i := range out { - outPatterns[i] = out[i].Pattern - } - - clean := func(s []string) []string { - if len(s) == 0 { - return nil - } - sort.Strings(s) - return s - } - - outPatterns = clean(outPatterns) - test.exp = clean(test.exp) - if !reflect.DeepEqual(outPatterns, test.exp) { - t.Fatalf("expected: %+v\ngot: %+v", test.exp, outPatterns) - } - }) - } -} diff --git a/accessctl/condition.go b/accessctl/condition.go deleted file mode 100644 index 7a56354..0000000 --- a/accessctl/condition.go +++ /dev/null @@ -1,130 +0,0 @@ -package accessctl - -import ( - "dehub/sigcred" - "dehub/typeobj" - "errors" - "fmt" - "math" - "strconv" - "strings" -) - -// ConditionInterface describes the methods that all Signifiers must implement. -type ConditionInterface interface { - - // Satisfied asserts that the Condition is satisfied by the given set of - // Credentials. If it is not (or something else went wrong) then an error is - // returned. - // - // NOTE that Satisfied assumes the Credential has already been Verify'd. - Satisfied([]sigcred.Credential) error -} - -// Condition represents an access control condition being defined in the Config. -// Only one of its fields may be filled in at a time. -type Condition struct { - Signature *ConditionSignature `type:"signature"` -} - -// MarshalYAML implements the yaml.Marshaler interface. -func (c Condition) MarshalYAML() (interface{}, error) { - return typeobj.MarshalYAML(c) -} - -// UnmarshalYAML implements the yaml.Unmarshaler interface. -func (c *Condition) UnmarshalYAML(unmarshal func(interface{}) error) error { - return typeobj.UnmarshalYAML(c, unmarshal) -} - -// Interface returns the ConditionInterface encapsulated by this Condition -// object. -func (c Condition) Interface() (ConditionInterface, error) { - el, _, err := typeobj.Element(c) - if err != nil { - return nil, err - } - return el.(ConditionInterface), nil -} - -// ConditionSignature represents the configuration of an access control -// condition which requires one or more signatures to be present on a commit. -// -// Either AccountIDs or AccountIDsByMeta must be filled. -type ConditionSignature struct { - AccountIDs []string `yaml:"account_ids,omitempty"` - AnyAccount bool `yaml:"any_account,omitempty"` - Count string `yaml:"count"` -} - -var _ ConditionInterface = ConditionSignature{} - -func (condSig ConditionSignature) targetNum() (int, error) { - if !strings.HasSuffix(condSig.Count, "%") { - return strconv.Atoi(condSig.Count) - } else if condSig.AnyAccount { - return 0, errors.New("cannot use AnyAccount and a percent Count together") - } - - percentStr := strings.TrimRight(condSig.Count, "%") - percent, err := strconv.ParseFloat(percentStr, 64) - if err != nil { - return 0, fmt.Errorf("could not parse Count as percent %q: %w", condSig.Count, err) - } - targetF := float64(len(condSig.AccountIDs)) * percent / 100 - targetF = math.Ceil(targetF) - return int(targetF), nil -} - -// ErrConditionSignatureUnsatisfied is returned from ConditionSignature's -// Satisfied method when the Condition has not been satisfied. -type ErrConditionSignatureUnsatisfied struct { - TargetNumAccounts, NumAccounts int -} - -func (err ErrConditionSignatureUnsatisfied) Error() string { - return fmt.Sprintf("not enough valid signature credentials, requires %d but only had %d", - err.TargetNumAccounts, err.NumAccounts) -} - -// Satisfied asserts that the given Credentials contains enough signatures to be -// satisfied. -func (condSig ConditionSignature) Satisfied(creds []sigcred.Credential) error { - targetN, err := condSig.targetNum() - if err != nil { - return fmt.Errorf("could not compute ConditionSignature target number of accounts: %w", err) - } - - credAccountIDs := map[string]struct{}{} - for _, cred := range creds { - // TODO currently only signature credentials are implemented, so we can - // just assume that the given AccountID has provided a sig. In the - // future this may not be true. - credAccountIDs[cred.AccountID] = struct{}{} - } - - var n int - if condSig.AnyAccount { - // TODO this doesn't actually check that the accounts are defined in the - // Config. - n = len(credAccountIDs) - } else { - targetAccountIDs := map[string]struct{}{} - for _, accountID := range condSig.AccountIDs { - targetAccountIDs[accountID] = struct{}{} - } - for accountID := range targetAccountIDs { - if _, ok := credAccountIDs[accountID]; ok { - n++ - } - } - } - - if n < targetN { - return ErrConditionSignatureUnsatisfied{ - TargetNumAccounts: targetN, - NumAccounts: n, - } - } - return nil -} diff --git a/accessctl/condition_test.go b/accessctl/condition_test.go deleted file mode 100644 index a20fc59..0000000 --- a/accessctl/condition_test.go +++ /dev/null @@ -1,110 +0,0 @@ -package accessctl - -import ( - "dehub/sigcred" - "reflect" - "testing" -) - -func TestConditionSignatureSatisfied(t *testing.T) { - tests := []struct { - descr string - cond ConditionSignature - credAccountIDs []string - err error - }{ - { - descr: "no cred accounts", - cond: ConditionSignature{ - AnyAccount: true, - Count: "1", - }, - err: ErrConditionSignatureUnsatisfied{ - TargetNumAccounts: 1, - NumAccounts: 0, - }, - }, - { - descr: "one cred account", - cond: ConditionSignature{ - AnyAccount: true, - Count: "1", - }, - credAccountIDs: []string{"foo"}, - }, - { - descr: "one matching cred account", - cond: ConditionSignature{ - AccountIDs: []string{"foo", "bar"}, - Count: "1", - }, - credAccountIDs: []string{"foo"}, - }, - { - descr: "no matching cred account", - cond: ConditionSignature{ - AccountIDs: []string{"foo", "bar"}, - Count: "1", - }, - credAccountIDs: []string{"baz"}, - err: ErrConditionSignatureUnsatisfied{ - TargetNumAccounts: 1, - NumAccounts: 0, - }, - }, - { - descr: "two matching cred accounts", - cond: ConditionSignature{ - AccountIDs: []string{"foo", "bar"}, - Count: "2", - }, - credAccountIDs: []string{"foo", "bar"}, - }, - { - descr: "one matching cred account, missing one", - cond: ConditionSignature{ - AccountIDs: []string{"foo", "bar"}, - Count: "2", - }, - credAccountIDs: []string{"foo", "baz"}, - err: ErrConditionSignatureUnsatisfied{ - TargetNumAccounts: 2, - NumAccounts: 1, - }, - }, - { - descr: "50 percent matching cred accounts", - cond: ConditionSignature{ - AccountIDs: []string{"foo", "bar", "baz"}, - Count: "50%", - }, - credAccountIDs: []string{"foo", "bar"}, - }, - { - descr: "not 50 percent matching cred accounts", - cond: ConditionSignature{ - AccountIDs: []string{"foo", "bar", "baz"}, - Count: "50%", - }, - credAccountIDs: []string{"foo"}, - err: ErrConditionSignatureUnsatisfied{ - TargetNumAccounts: 2, - NumAccounts: 1, - }, - }, - } - - for _, test := range tests { - t.Run(test.descr, func(t *testing.T) { - creds := make([]sigcred.Credential, len(test.credAccountIDs)) - for i := range test.credAccountIDs { - creds[i].AccountID = test.credAccountIDs[i] - } - - err := test.cond.Satisfied(creds) - if !reflect.DeepEqual(err, test.err) { - t.Fatalf("Satisfied returned %#v\nexpected %#v", err, test.err) - } - }) - } -} diff --git a/change_hash.go b/change_hash.go deleted file mode 100644 index 8c4feb0..0000000 --- a/change_hash.go +++ /dev/null @@ -1,75 +0,0 @@ -package dehub - -import ( - "crypto/sha256" - "encoding/binary" - "fmt" - "hash" - "sort" - - "gopkg.in/src-d/go-git.v4/plumbing/object" -) - -var ( - defaultHashHelperAlgo = sha256.New -) - -type hashHelper struct { - hash.Hash - varintBuf []byte -} - -// if h is nil it then defaultHashHelperAlgo will be used -func newHashHelper(h hash.Hash) *hashHelper { - if h == nil { - h = defaultHashHelperAlgo() - } - s := &hashHelper{ - Hash: h, - varintBuf: make([]byte, binary.MaxVarintLen64), - } - return s -} - -func (s *hashHelper) writeUint(i uint64) { - n := binary.PutUvarint(s.varintBuf, i) - if _, err := s.Write(s.varintBuf[:n]); err != nil { - panic(fmt.Sprintf("error writing %x to sha256 sum: %v", s.varintBuf[:n], err)) - } -} - -func (s *hashHelper) writeStr(str string) { - s.writeUint(uint64(len(str))) - s.Write([]byte(str)) -} - -func (s *hashHelper) writeTreeDiff(from, to *object.Tree) { - filesChanged, err := calcDiff(from, to) - if err != nil { - panic(err.Error()) - } - - sort.Slice(filesChanged, func(i, j int) bool { - return filesChanged[i].path < filesChanged[j].path - }) - - s.writeUint(uint64(len(filesChanged))) - for _, fileChanged := range filesChanged { - s.writeStr(fileChanged.path) - s.Write(fileChanged.fromMode.Bytes()) - s.Write(fileChanged.fromHash[:]) - s.Write(fileChanged.toMode.Bytes()) - s.Write(fileChanged.toHash[:]) - } - -} - -var changeHashVersion = []byte{0} - -// if h is nil it then defaultHashHelperAlgo will be used -func genChangeHash(h hash.Hash, msg string, from, to *object.Tree) []byte { - s := newHashHelper(h) - s.writeStr(msg) - s.writeTreeDiff(from, to) - return s.Sum(changeHashVersion) -} diff --git a/cmd/dehub/main.go b/cmd/dehub/main.go deleted file mode 100644 index 431fc0a..0000000 --- a/cmd/dehub/main.go +++ /dev/null @@ -1,136 +0,0 @@ -package main - -import ( - "dehub" - "errors" - "flag" - "fmt" - "os" - "strings" - - "gopkg.in/src-d/go-git.v4/plumbing" -) - -type subCmdCtx struct { - repo *dehub.Repo - args []string -} - -var subCmds = []struct { - name, descr string - body func(sctx subCmdCtx) error -}{ - { - name: "commit", - descr: "commits staged changes to the head of the current branch", - body: func(sctx subCmdCtx) error { - flag := flag.NewFlagSet("commit", flag.ExitOnError) - msg := flag.String("msg", "", "Commit message to use") - accountID := flag.String("account-id", "", "Account to sign commit as") - flag.Parse(sctx.args) - - if *msg == "" || *accountID == "" { - return errors.New("-msg and -account-id are both required") - } - - cfg, err := sctx.repo.LoadConfig() - if err != nil { - return err - } - - var account dehub.Account - var ok bool - for _, account = range cfg.Accounts { - if account.ID == *accountID { - ok = true - break - } - } - if !ok { - return fmt.Errorf("account ID %q not found in config", *accountID) - } else if l := len(account.Signifiers); l == 0 || l > 1 { - return fmt.Errorf("account %q has %d signifiers, only one is supported right now", *accountID, l) - } - - sig := account.Signifiers[0] - sigInt, err := sig.Interface() - if err != nil { - return fmt.Errorf("could not cast %+v to SignifierInterface: %w", sig, err) - } - - _, hash, err := sctx.repo.CommitMaster(*msg, *accountID, sigInt) - if err != nil { - return err - } - fmt.Printf("changes committed to HEAD as %s\n", hash) - return nil - }, - }, - { - name: "verify", - descr: "verifies one or more commits as having the proper credentials", - body: func(sctx subCmdCtx) error { - flag := flag.NewFlagSet("verify", flag.ExitOnError) - rev := flag.String("rev", "HEAD", "Revision of commit to verify") - flag.Parse(sctx.args) - - h, err := sctx.repo.GitRepo.ResolveRevision(plumbing.Revision(*rev)) - if err != nil { - return fmt.Errorf("could not resolve revision %q: %w", *rev, err) - } - - if err := sctx.repo.VerifyMasterCommit(*h); err != nil { - return fmt.Errorf("could not verify commit at %q (%s): %w", *rev, *h, err) - } - - fmt.Printf("commit at %q (%s) is good to go!\n", *rev, *h) - return nil - }, - }, -} - -func printHelp() { - fmt.Printf("USAGE: %s [-h]\n\n", os.Args[0]) - fmt.Println("COMMANDS") - for _, subCmd := range subCmds { - fmt.Printf("\t%s : %s\n", subCmd.name, subCmd.descr) - } -} - -func exitErr(err error) { - fmt.Fprintf(os.Stderr, "exiting: %v\n", err) - os.Stderr.Sync() - os.Stdout.Sync() - os.Exit(1) -} - -func main() { - if len(os.Args) < 2 { - printHelp() - return - } - - subCmdName := strings.ToLower(os.Args[1]) - for _, subCmd := range subCmds { - if subCmd.name != subCmdName { - continue - } - - r, err := dehub.OpenRepo(".") - if err != nil { - exitErr(err) - } - - err = subCmd.body(subCmdCtx{ - repo: r, - args: os.Args[2:], - }) - if err != nil { - exitErr(err) - } - return - } - - fmt.Printf("unknown command %q\n\n", subCmdName) - printHelp() -} diff --git a/commit.go b/commit.go deleted file mode 100644 index 10b4739..0000000 --- a/commit.go +++ /dev/null @@ -1,251 +0,0 @@ -package dehub - -import ( - "bytes" - "dehub/accessctl" - "dehub/fs" - "dehub/sigcred" - "dehub/yamlutil" - "encoding/base64" - "errors" - "fmt" - "strings" - "time" - - "gopkg.in/src-d/go-git.v4" - "gopkg.in/src-d/go-git.v4/plumbing" - "gopkg.in/src-d/go-git.v4/plumbing/object" - yaml "gopkg.in/yaml.v2" -) - -// MasterCommit describes the structure of the object encoded into the git -// message of a commit in the master branch. -type MasterCommit struct { - Message string `yaml:"message"` - ChangeHash yamlutil.Blob `yaml:"change_hash"` - Credentials []sigcred.Credential `yaml:"credentials"` -} - -type mcYAML struct { - Val MasterCommit `yaml:",inline"` -} - -func msgHead(msg string) string { - i := strings.Index(msg, "\n") - if i > 0 { - return msg[:i] - } - return msg -} - -// MarshalText implements the encoding.TextMarshaler interface by returning the -// form the MasterCommit object takes in the git commit message. -func (mc MasterCommit) MarshalText() ([]byte, error) { - masterCommitEncoded, err := yaml.Marshal(mcYAML{mc}) - if err != nil { - return nil, fmt.Errorf("failed to encode MasterCommit message: %w", err) - } - - fullMsg := msgHead(mc.Message) + "\n\n" + string(masterCommitEncoded) - return []byte(fullMsg), nil -} - -// UnmarshalText implements the encoding.TextUnmarshaler interface by decoding a -// MasterCommit object which has been encoded into a git commit message. -func (mc *MasterCommit) UnmarshalText(msg []byte) error { - i := bytes.Index(msg, []byte("\n")) - if i < 0 { - return fmt.Errorf("commit message %q is malformed", msg) - } - msgHead, msg := msg[:i], msg[i:] - - var mcy mcYAML - if err := yaml.Unmarshal(msg, &mcy); err != nil { - return fmt.Errorf("could not unmarshal MasterCommit message: %w", err) - } - - *mc = mcy.Val - if !strings.HasPrefix(mc.Message, string(msgHead)) { - return errors.New("encoded MasterCommit is malformed, it might not be an encoded MasterCommit") - } - - return nil -} - -// CommitMaster constructs a MasterCommit using the given SignifierInterface to -// create a Credential for it. It returns the commit's hash after having set it -// to HEAD. -// -// TODO this method is a prototype and does not reflect the method's final form. -func (r *Repo) CommitMaster(msg, accountID string, sig sigcred.SignifierInterface) (MasterCommit, plumbing.Hash, error) { - _, headTree, err := r.head() - if errors.Is(err, plumbing.ErrReferenceNotFound) { - headTree = &object.Tree{} - } else if err != nil { - return MasterCommit{}, plumbing.ZeroHash, err - } - - _, stagedTree, err := fs.FromStagedChangesTree(r.GitRepo) - if err != nil { - return MasterCommit{}, plumbing.ZeroHash, err - } - - // this is necessarily different than headTree for the case of there being - // no HEAD (ie it's the first commit). In that case we want headTree to be - // empty (because it's being used to generate the change hash), but we want - // the signifier to use the raw fs (because that's where the signifier's - // data might be). - sigFS, err := r.headOrRawFS() - if err != nil { - return MasterCommit{}, plumbing.ZeroHash, err - } - - cfg, err := r.loadConfig(sigFS) - if err != nil { - return MasterCommit{}, plumbing.ZeroHash, fmt.Errorf("could not load config: %w", err) - } - - changeHash := genChangeHash(nil, msg, headTree, stagedTree) - cred, err := sig.Sign(sigFS, changeHash) - if err != nil { - return MasterCommit{}, plumbing.ZeroHash, fmt.Errorf("failed to sign commit hash: %w", err) - } - cred.AccountID = accountID - - // This isn't strictly necessary, but we want to save people the effort of - // creating an invalid commit, pushing it, having it be rejected, then - // having to reset on the commit. - err = r.assertAccessControls( - cfg.AccessControls, []sigcred.Credential{cred}, - headTree, stagedTree, - ) - if err != nil { - return MasterCommit{}, plumbing.ZeroHash, fmt.Errorf("commit would not satisfy access controls: %w", err) - } - - masterCommit := MasterCommit{ - Message: msg, - ChangeHash: changeHash, - Credentials: []sigcred.Credential{cred}, - } - - masterCommitB, err := masterCommit.MarshalText() - if err != nil { - return masterCommit, plumbing.ZeroHash, err - } - - w, err := r.GitRepo.Worktree() - if err != nil { - return masterCommit, plumbing.ZeroHash, fmt.Errorf("could not get git worktree: %w", err) - } - - hash, err := w.Commit(string(masterCommitB), &git.CommitOptions{ - Author: &object.Signature{ - Name: accountID, - When: time.Now(), - }, - }) - if err != nil { - return masterCommit, hash, fmt.Errorf("failed to commit changed: %w", err) - } - - return masterCommit, hash, nil - -} - -func (r *Repo) assertAccessControls( - accessCtls []accessctl.AccessControl, creds []sigcred.Credential, - from, to *object.Tree, -) error { - filesChanged, err := calcDiff(from, to) - if err != nil { - return err - } - - pathsChanged := make([]string, len(filesChanged)) - for i := range filesChanged { - pathsChanged[i] = filesChanged[i].path - } - - accessCtls, err = accessctl.ApplicableAccessControls(accessCtls, pathsChanged) - if err != nil { - return fmt.Errorf("could not determine applicable access controls: %w", err) - } - - for _, accessCtl := range accessCtls { - condInt, err := accessCtl.Condition.Interface() - if err != nil { - return fmt.Errorf("could not cast Condition to interface: %w", err) - } else if err := condInt.Satisfied(creds); err != nil { - return fmt.Errorf("access control for pattern %q not satisfied: %w", - accessCtl.Pattern, err) - } - } - - return nil -} - -// VerifyMasterCommit verifies that the commit at the given hash, which is -// presumably on the master branch, is gucci. -func (r *Repo) VerifyMasterCommit(h plumbing.Hash) error { - commit, err := r.GitRepo.CommitObject(h) - if err != nil { - return fmt.Errorf("could not retrieve commit object: %w", err) - } - - commitTree, err := r.GitRepo.TreeObject(commit.TreeHash) - if err != nil { - return fmt.Errorf("could not retrieve tree object: %w", err) - } - - var masterCommit MasterCommit - if err := masterCommit.UnmarshalText([]byte(commit.Message)); err != nil { - return err - } - - sigTree := commitTree // only for root commit - parentTree := &object.Tree{} - if commit.NumParents() > 0 { - parent, err := commit.Parent(0) - if err != nil { - return fmt.Errorf("could not retrieve parent of commit: %w", err) - } else if parentTree, err = r.GitRepo.TreeObject(parent.TreeHash); err != nil { - return fmt.Errorf("could not retrieve tree object of parent %q: %w", parent.Hash, err) - } - sigTree = parentTree - } - sigFS := fs.FromTree(sigTree) - - cfg, err := r.loadConfig(sigFS) - if err != nil { - return fmt.Errorf("error loading config: %w", err) - } - - err = r.assertAccessControls( - cfg.AccessControls, masterCommit.Credentials, - parentTree, commitTree, - ) - if err != nil { - return fmt.Errorf("failed to satisfy all access controls: %w", err) - } - - expectedChangeHash := genChangeHash(nil, masterCommit.Message, parentTree, commitTree) - if !bytes.Equal(masterCommit.ChangeHash, expectedChangeHash) { - return fmt.Errorf("malformed change_hash in commit body, is %s but should be %s", - base64.StdEncoding.EncodeToString(expectedChangeHash), - base64.StdEncoding.EncodeToString(masterCommit.ChangeHash)) - } - - for _, cred := range masterCommit.Credentials { - sig, err := r.signifierForCredential(sigFS, cred) - if err != nil { - return fmt.Errorf("error finding signifier for credential %+v: %w", cred, err) - } else if err := sig.Verify(sigFS, expectedChangeHash, cred); err != nil { - return fmt.Errorf("error verifying credential %+v: %w", cred, err) - } - } - - // TODO access controls - - return nil -} diff --git a/commit_test.go b/commit_test.go deleted file mode 100644 index 2f2549d..0000000 --- a/commit_test.go +++ /dev/null @@ -1,170 +0,0 @@ -package dehub - -import ( - "dehub/accessctl" - "dehub/sigcred" - "errors" - "reflect" - "strings" - "testing" - - "github.com/davecgh/go-spew/spew" - "gopkg.in/src-d/go-git.v4/plumbing" - yaml "gopkg.in/yaml.v2" -) - -func TestMasterCommitVerify(t *testing.T) { - type step struct { - msg string - msgHead string // defaults to msg - tree map[string]string - } - testCases := []struct { - descr string - steps []step - }{ - { - descr: "single commit", - steps: []step{ - { - msg: "first commit", - tree: map[string]string{"a": "0", "b": "1"}, - }, - }, - }, - { - descr: "multiple commits", - steps: []step{ - { - msg: "first commit", - tree: map[string]string{"a": "0", "b": "1"}, - }, - { - msg: "second commit, changing a", - tree: map[string]string{"a": "1"}, - }, - { - msg: "third commit, empty", - }, - { - msg: "fourth commit, adding c, removing b", - tree: map[string]string{"b": "", "c": "2"}, - }, - }, - }, - { - descr: "big body commits", - steps: []step{ - { - msg: "first commit, single line but with newline\n", - }, - { - msg: "second commit, single line but with two newlines\n\n", - msgHead: "second commit, single line but with two newlines\n\n", - }, - { - msg: "third commit, multi-line with one newline\nanother line!", - msgHead: "third commit, multi-line with one newline\n\n", - }, - { - msg: "fourth commit, multi-line with two newlines\n\nanother line!", - msgHead: "fourth commit, multi-line with two newlines\n\n", - }, - }, - }, - } - - for _, test := range testCases { - t.Run(test.descr, func(t *testing.T) { - h := newHarness(t) - for _, step := range test.steps { - h.stage(step.tree) - account := h.cfg.Accounts[0] - - masterCommit, hash, err := h.repo.CommitMaster(step.msg, account.ID, h.sig) - if err != nil { - t.Fatalf("failed to make MasterCommit: %v", err) - } else if err := h.repo.VerifyMasterCommit(hash); err != nil { - t.Fatalf("could not verify hash %v: %v", hash, err) - } - - commit, err := h.repo.GitRepo.CommitObject(hash) - if err != nil { - t.Fatalf("failed to retrieve commit %v: %v", hash, err) - } else if step.msgHead == "" { - step.msgHead = strings.TrimSpace(step.msg) + "\n\n" - } - - if !strings.HasPrefix(commit.Message, step.msgHead) { - t.Fatalf("commit message %q does not start with expected head %q", commit.Message, step.msgHead) - } - - var actualMasterCommit MasterCommit - if err := actualMasterCommit.UnmarshalText([]byte(commit.Message)); err != nil { - t.Fatalf("error unmarshaling commit body: %v", err) - } else if !reflect.DeepEqual(actualMasterCommit, masterCommit) { - t.Fatalf("returned master commit:\n%s\ndoes not match actual one:\n%s", - spew.Sdump(masterCommit), spew.Sdump(actualMasterCommit)) - } - } - }) - } -} - -func TestConfigChange(t *testing.T) { - h := newHarness(t) - - var hashes []plumbing.Hash - - // commit the initial staged changes, which merely include the config and - // public key - _, hash, err := h.repo.CommitMaster("commit configuration", h.cfg.Accounts[0].ID, h.sig) - if err != nil { - t.Fatal(err) - } - hashes = append(hashes, hash) - - // create a new account and add it to the configuration. It should not be - // able to actually make that commit though. - newSig, newPubKeyBody := sigcred.SignifierPGPTmp(h.rand) - h.cfg.Accounts = append(h.cfg.Accounts, Account{ - ID: "toot", - Signifiers: []sigcred.Signifier{{PGPPublicKey: &sigcred.SignifierPGP{ - Body: string(newPubKeyBody), - }}}, - }) - h.cfg.AccessControls[0].Condition.Signature.AccountIDs = []string{"root", "toot"} - h.cfg.AccessControls[0].Condition.Signature.Count = "1" - - cfgBody, err := yaml.Marshal(h.cfg) - if err != nil { - t.Fatal(err) - } - h.stage(map[string]string{ConfigPath: string(cfgBody)}) - - _, _, err = h.repo.CommitMaster("add toot user", h.cfg.Accounts[1].ID, newSig) - if aclErr := (accessctl.ErrConditionSignatureUnsatisfied{}); !errors.As(err, &aclErr) { - t.Fatalf("CommitMaster should have returned an ErrConditionSignatureUnsatisfied, but returned %v", err) - } - - // now add with the root user, this should work. - _, hash, err = h.repo.CommitMaster("add toot user", h.cfg.Accounts[0].ID, h.sig) - if err != nil { - t.Fatalf("got an unexpected error committing with root: %v", err) - } - hashes = append(hashes, hash) - - // _now_ the toot user should be able to do things. - h.stage(map[string]string{"foo/bar": "what a cool file"}) - _, hash, err = h.repo.CommitMaster("add a cool file", h.cfg.Accounts[1].ID, newSig) - if err != nil { - t.Fatalf("got an unexpected error committing with toot: %v", err) - } - hashes = append(hashes, hash) - - for i, hash := range hashes { - if err := h.repo.VerifyMasterCommit(hash); err != nil { - t.Fatalf("commit %d (%v) should have been verified but wasn't: %v", i, hash, err) - } - } -} diff --git a/config.go b/config.go deleted file mode 100644 index 62901c4..0000000 --- a/config.go +++ /dev/null @@ -1,83 +0,0 @@ -package dehub - -import ( - "dehub/accessctl" - "dehub/fs" - "dehub/sigcred" - "errors" - "fmt" - - yaml "gopkg.in/yaml.v2" -) - -// Account represents a single account defined in the Config. -type Account struct { - ID string `yaml:"id"` - Signifiers []sigcred.Signifier `yaml:"signifiers"` - Meta map[string]string `yaml:"meta,omitempty"` -} - -// Config represents the structure of the main dehub configuration file, and is -// used to marshal/unmarshal the yaml file. -type Config struct { - Accounts []Account `yaml:"accounts"` - AccessControls []accessctl.AccessControl `yaml:"access_controls"` -} - -func (r *Repo) loadConfig(fs fs.FS) (Config, error) { - rc, err := fs.Open(ConfigPath) - if err != nil { - return Config{}, fmt.Errorf("could not open config.yml: %w", err) - } - defer rc.Close() - - var cfg Config - if err := yaml.NewDecoder(rc).Decode(&cfg); err != nil { - return cfg, fmt.Errorf("could not decode config.yml: %w", err) - } - - // TODO validate Config - - return cfg, nil -} - -// LoadConfig loads the Config object from the HEAD of the repo, or directly -// from the filesystem if there is no HEAD yet. -func (r *Repo) LoadConfig() (Config, error) { - headFS, err := r.headOrRawFS() - if err != nil { - return Config{}, fmt.Errorf("error retrieving repo HEAD: %w", err) - } - return r.loadConfig(headFS) -} - -func (r *Repo) signifierForCredential(fs fs.FS, cred sigcred.Credential) (sigcred.SignifierInterface, error) { - cfg, err := r.loadConfig(fs) - if err != nil { - return nil, fmt.Errorf("error loading config: %w", err) - } - - var account Account - var ok bool - for _, account = range cfg.Accounts { - if account.ID == cred.AccountID { - ok = true - break - } - } - if !ok { - return nil, fmt.Errorf("no account object for account id %q present in config", cred.AccountID) - } - - for i, sig := range account.Signifiers { - if sigInt, err := sig.Interface(); err != nil { - return nil, fmt.Errorf("error converting signifier index:%d to inteface: %w", i, err) - } else if ok, err := sigInt.Signed(fs, cred); err != nil { - return nil, fmt.Errorf("error checking if signfier index:%d signed credential: %w", i, err) - } else if ok { - return sigInt, nil - } - } - - return nil, errors.New("no signifier found for credential") -} diff --git a/diff.go b/diff.go deleted file mode 100644 index d3aaa32..0000000 --- a/diff.go +++ /dev/null @@ -1,41 +0,0 @@ -package dehub - -import ( - "fmt" - - "gopkg.in/src-d/go-git.v4/plumbing" - "gopkg.in/src-d/go-git.v4/plumbing/filemode" - "gopkg.in/src-d/go-git.v4/plumbing/object" -) - -type fileChanged struct { - path string - fromMode, toMode filemode.FileMode - fromHash, toHash plumbing.Hash -} - -func calcDiff(from, to *object.Tree) ([]fileChanged, error) { - - changes, err := object.DiffTree(from, to) - if err != nil { - return nil, fmt.Errorf("could not calculate tree diff: %w", err) - } - - filesChanged := make([]fileChanged, len(changes)) - for i, change := range changes { - if from := change.From; from.Name != "" { - filesChanged[i].path = from.Name - filesChanged[i].fromMode = from.TreeEntry.Mode - filesChanged[i].fromHash = from.TreeEntry.Hash - } - if to := change.To; to.Name != "" { - if exPath := filesChanged[i].path; exPath != "" && exPath != to.Name { - panic(fmt.Sprintf("DiffTree entry changed path from %q to %q", exPath, to.Name)) - } - filesChanged[i].path = to.Name - filesChanged[i].toMode = to.TreeEntry.Mode - filesChanged[i].toHash = to.TreeEntry.Hash - } - } - return filesChanged, nil -} diff --git a/fs/build_tree_helper.go b/fs/build_tree_helper.go deleted file mode 100644 index 3aaafe8..0000000 --- a/fs/build_tree_helper.go +++ /dev/null @@ -1,117 +0,0 @@ -package fs - -import ( - "path" - "sort" - "strings" - - "gopkg.in/src-d/go-billy.v4" - "gopkg.in/src-d/go-git.v4/plumbing" - "gopkg.in/src-d/go-git.v4/plumbing/filemode" - "gopkg.in/src-d/go-git.v4/plumbing/format/index" - "gopkg.in/src-d/go-git.v4/plumbing/object" - "gopkg.in/src-d/go-git.v4/storage" -) - -// This file is largely copied from the git-go project's worktree_commit.go @ v4.13.1 - -// buildTreeHelper converts a given index.Index file into multiple git objects -// reading the blobs from the given filesystem and creating the trees from the -// index structure. The created objects are pushed to a given Storer. -type buildTreeHelper struct { - fs billy.Filesystem - s storage.Storer - - trees map[string]*object.Tree - entries map[string]*object.TreeEntry -} - -// BuildTree builds the tree objects and push its to the storer, the hash -// of the root tree is returned. -func (h *buildTreeHelper) BuildTree(idx *index.Index) (plumbing.Hash, error) { - const rootNode = "" - h.trees = map[string]*object.Tree{rootNode: {}} - h.entries = map[string]*object.TreeEntry{} - - for _, e := range idx.Entries { - if err := h.commitIndexEntry(e); err != nil { - return plumbing.ZeroHash, err - } - } - - return h.copyTreeToStorageRecursive(rootNode, h.trees[rootNode]) -} - -func (h *buildTreeHelper) commitIndexEntry(e *index.Entry) error { - parts := strings.Split(e.Name, "/") - - var fullpath string - for _, part := range parts { - parent := fullpath - fullpath = path.Join(fullpath, part) - - h.doBuildTree(e, parent, fullpath) - } - - return nil -} - -func (h *buildTreeHelper) doBuildTree(e *index.Entry, parent, fullpath string) { - if _, ok := h.trees[fullpath]; ok { - return - } - - if _, ok := h.entries[fullpath]; ok { - return - } - - te := object.TreeEntry{Name: path.Base(fullpath)} - - if fullpath == e.Name { - te.Mode = e.Mode - te.Hash = e.Hash - } else { - te.Mode = filemode.Dir - h.trees[fullpath] = &object.Tree{} - } - - h.trees[parent].Entries = append(h.trees[parent].Entries, te) -} - -type sortableEntries []object.TreeEntry - -func (sortableEntries) sortName(te object.TreeEntry) string { - if te.Mode == filemode.Dir { - return te.Name + "/" - } - return te.Name -} -func (se sortableEntries) Len() int { return len(se) } -func (se sortableEntries) Less(i int, j int) bool { return se.sortName(se[i]) < se.sortName(se[j]) } -func (se sortableEntries) Swap(i int, j int) { se[i], se[j] = se[j], se[i] } - -func (h *buildTreeHelper) copyTreeToStorageRecursive(parent string, t *object.Tree) (plumbing.Hash, error) { - sort.Sort(sortableEntries(t.Entries)) - for i, e := range t.Entries { - if e.Mode != filemode.Dir && !e.Hash.IsZero() { - continue - } - - path := path.Join(parent, e.Name) - - var err error - e.Hash, err = h.copyTreeToStorageRecursive(path, h.trees[path]) - if err != nil { - return plumbing.ZeroHash, err - } - - t.Entries[i] = e - } - - o := h.s.NewEncodedObject() - if err := t.Encode(o); err != nil { - return plumbing.ZeroHash, err - } - - return h.s.SetEncodedObject(o) -} diff --git a/fs/fs.go b/fs/fs.go deleted file mode 100644 index 5905268..0000000 --- a/fs/fs.go +++ /dev/null @@ -1,100 +0,0 @@ -// Package fs implements abstractions for interacting with a filesystem, either -// via a git tree, a staged index, or directly. -package fs - -import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "os" - - "gopkg.in/src-d/go-billy.v4" - "gopkg.in/src-d/go-git.v4" - "gopkg.in/src-d/go-git.v4/plumbing/object" -) - -// FS is a simple interface for reading off a snapshot of a filesystem. -type FS interface { - Open(path string) (io.ReadCloser, error) -} - -type treeFS struct { - tree *object.Tree -} - -// FromTree wraps a git tree object to implement the FS interface. All paths -// will be relative to the root of the tree. -func FromTree(t *object.Tree) FS { - return treeFS{tree: t} -} - -func (gt treeFS) Open(path string) (io.ReadCloser, error) { - f, err := gt.tree.File(path) - if err != nil { - return nil, err - } - - return f.Blob.Reader() -} - -type billyFS struct { - fs billy.Filesystem -} - -// FromBillyFilesystem wraps a billy.Filesystem to implement the FS interface. -// All paths will be relative to the filesystem's root. -func FromBillyFilesystem(bfs billy.Filesystem) FS { - return billyFS{fs: bfs} -} - -func (bfs billyFS) Open(path string) (io.ReadCloser, error) { - return bfs.fs.Open(path) -} - -// FromStagedChangesTree processes the current set of staged changes into a tree -// object, and returns an FS for that tree. All paths will be relative to the -// root of the git repo. -func FromStagedChangesTree(repo *git.Repository) (FS, *object.Tree, error) { - w, err := repo.Worktree() - if err != nil { - return nil, nil, fmt.Errorf("could not open git worktree: %w", err) - } - - storer := repo.Storer - idx, err := storer.Index() - if err != nil { - return nil, nil, fmt.Errorf("could not open git staging index: %w", err) - } - - th := &buildTreeHelper{ - fs: w.Filesystem, - s: storer, - } - - treeHash, err := th.BuildTree(idx) - if err != nil { - return nil, nil, fmt.Errorf("could not build staging index tree: %w", err) - } - - tree, err := repo.TreeObject(treeHash) - if err != nil { - return nil, nil, fmt.Errorf("could not get staged tree object (%q): %w", treeHash, err) - } - - return FromTree(tree), tree, nil -} - -// Stub is an implementation of FS based on a map of paths to the file contents -// at that path. Paths should be "clean" or they will not match with anything. -type Stub map[string][]byte - -// Open implements the method for the FS interface. -func (s Stub) Open(path string) (io.ReadCloser, error) { - body, ok := s[path] - if !ok { - return nil, os.ErrNotExist - } - - return ioutil.NopCloser(bytes.NewReader(body)), nil -} diff --git a/go.mod b/go.mod deleted file mode 100644 index e86a563..0000000 --- a/go.mod +++ /dev/null @@ -1,12 +0,0 @@ -module dehub - -go 1.13 - -require ( - github.com/bmatcuk/doublestar v1.2.2 - github.com/davecgh/go-spew v1.1.1 - golang.org/x/crypto v0.0.0-20200109152110-61a87790db17 - gopkg.in/src-d/go-billy.v4 v4.3.2 - gopkg.in/src-d/go-git.v4 v4.13.1 - gopkg.in/yaml.v2 v2.2.7 -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 1ea192f..0000000 --- a/go.sum +++ /dev/null @@ -1,82 +0,0 @@ -github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= -github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/bmatcuk/doublestar v1.2.2 h1:oC24CykoSAB8zd7XgruHo33E0cHJf/WhQA/7BeXj+x0= -github.com/bmatcuk/doublestar v1.2.2/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= -github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= -github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= -github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= -github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= -github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= -golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200109152110-61a87790db17 h1:nVJ3guKA9qdkEQ3TUdXI9QSINo2CUPM/cySEvw2w8I0= -golang.org/x/crypto v0.0.0-20200109152110-61a87790db17/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190729092621-ff9f1409240a/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg= -gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= -gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg= -gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= -gopkg.in/src-d/go-git.v4 v4.13.1 h1:SRtFyV8Kxc0UP7aCHcijOMQGPxHSmMOPrzulQWolkYE= -gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQbYDu2z8= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= -gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/repo.go b/repo.go deleted file mode 100644 index 67c6d4d..0000000 --- a/repo.go +++ /dev/null @@ -1,104 +0,0 @@ -// Package dehub TODO needs package docs -package dehub - -import ( - "dehub/fs" - "errors" - "fmt" - "path/filepath" - - "gopkg.in/src-d/go-billy.v4" - "gopkg.in/src-d/go-billy.v4/memfs" - "gopkg.in/src-d/go-git.v4" - "gopkg.in/src-d/go-git.v4/plumbing" - "gopkg.in/src-d/go-git.v4/plumbing/object" - "gopkg.in/src-d/go-git.v4/storage/memory" -) - -const ( - // DehubDir defines the name of the directory where all dehub-related files are - // expected to be found. - DehubDir = ".dehub" -) - -var ( - // ConfigPath defines the expected path to the Repo's configuration file. - ConfigPath = filepath.Join(DehubDir, "config.yml") -) - -// Repo is an object which allows accessing and modifying the dehub repo. -type Repo struct { - GitRepo *git.Repository -} - -// OpenRepo opens the dehub repo in the given directory and returns the object -// for it. -// -// The given path is expected to have a git repo and .dehub folder already -// initialized. -func OpenRepo(path string) (*Repo, error) { - r := Repo{} - var err error - openOpts := &git.PlainOpenOptions{ - DetectDotGit: true, - } - if r.GitRepo, err = git.PlainOpenWithOptions(path, openOpts); err != nil { - return nil, fmt.Errorf("could not open git repo: %w", err) - } - return &r, nil -} - -// InitMemRepo initializes an empty repository which only exists in memory. -func InitMemRepo() *Repo { - r, err := git.Init(memory.NewStorage(), memfs.New()) - if err != nil { - panic(err) - } - - return &Repo{GitRepo: r} -} - -func (r *Repo) billyFilesystem() (billy.Filesystem, error) { - w, err := r.GitRepo.Worktree() - if err != nil { - return nil, fmt.Errorf("could not open git worktree: %w", err) - } - return w.Filesystem, nil -} - -func (r *Repo) head() (*object.Commit, *object.Tree, error) { - head, err := r.GitRepo.Head() - if err != nil { - return nil, nil, fmt.Errorf("could not get repo HEAD: %w", err) - } - - headHash := head.Hash() - headCommit, err := r.GitRepo.CommitObject(headHash) - if err != nil { - return nil, nil, fmt.Errorf("could not get commit at HEAD (%q): %w", headHash, err) - } - - headTree, err := r.GitRepo.TreeObject(headCommit.TreeHash) - if err != nil { - return nil, nil, fmt.Errorf("could not get tree object at HEAD (commit:%q tree:%q): %w", - headHash, headCommit.TreeHash, err) - } - - return headCommit, headTree, nil -} - -// headOrRawFS returns an FS based on the HEAD commit, or if there is no HEAD -// commit (it's an empty repo) an FS based on the raw filesystem. -func (r *Repo) headOrRawFS() (fs.FS, error) { - _, headTree, err := r.head() - if errors.Is(err, plumbing.ErrReferenceNotFound) { - bfs, err := r.billyFilesystem() - if err != nil { - return nil, fmt.Errorf("could not get underlying filesystem: %w", err) - } - return fs.FromBillyFilesystem(bfs), nil - } else if err != nil { - return nil, fmt.Errorf("could not get HEAD tree: %w", err) - } - return fs.FromTree(headTree), nil -} diff --git a/repo_test.go b/repo_test.go deleted file mode 100644 index a187b67..0000000 --- a/repo_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package dehub - -import ( - "bytes" - "dehub/accessctl" - "dehub/sigcred" - "io" - "math/rand" - "path/filepath" - "testing" - - "gopkg.in/src-d/go-git.v4" - "gopkg.in/src-d/go-git.v4/plumbing" - "gopkg.in/src-d/go-git.v4/plumbing/object" - yaml "gopkg.in/yaml.v2" -) - -type harness struct { - t *testing.T - rand *rand.Rand - repo *Repo - cfg *Config - sig sigcred.SignifierInterface -} - -func newHarness(t *testing.T) *harness { - rand := rand.New(rand.NewSource(0xb4eadb01)) - sig, pubKeyBody := sigcred.SignifierPGPTmp(rand) - pubKeyPath := filepath.Join(DehubDir, "root.asc") - - cfg := &Config{ - Accounts: []Account{{ - ID: "root", - Signifiers: []sigcred.Signifier{{PGPPublicKeyFile: &sigcred.SignifierPGPFile{ - Path: pubKeyPath, - }}}, - }}, - AccessControls: []accessctl.AccessControl{ - { - Pattern: "**", - Condition: accessctl.Condition{ - Signature: &accessctl.ConditionSignature{ - AccountIDs: []string{"root"}, - Count: "100%", - }, - }, - }, - }, - } - cfgBody, err := yaml.Marshal(cfg) - if err != nil { - t.Fatal(err) - } - - h := &harness{ - t: t, - rand: rand, - repo: InitMemRepo(), - cfg: cfg, - sig: sig, - } - h.stage(map[string]string{ - ConfigPath: string(cfgBody), - pubKeyPath: string(pubKeyBody), - }) - - return h -} - -func (h *harness) stage(tree map[string]string) { - w, err := h.repo.GitRepo.Worktree() - if err != nil { - h.t.Fatal(err) - } - fs := w.Filesystem - for path, content := range tree { - if content == "" { - if _, err := w.Remove(path); err != nil { - h.t.Fatalf("error removing %q: %v", path, err) - } - continue - } - - dir := filepath.Dir(path) - if err := fs.MkdirAll(dir, 0666); err != nil { - h.t.Fatalf("error making directory %q: %v", dir, err) - } - - f, err := fs.Create(path) - if err != nil { - h.t.Fatalf("error creating file %q: %v", path, err) - - } else if _, err := io.Copy(f, bytes.NewBufferString(content)); err != nil { - h.t.Fatalf("error writing to file %q: %v", path, err) - - } else if err := f.Close(); err != nil { - h.t.Fatalf("error closing file %q: %v", path, err) - - } else if _, err := w.Add(path); err != nil { - h.t.Fatalf("error adding file %q to index: %v", path, err) - } - } -} - -func (h *harness) commit(msg string) plumbing.Hash { - w, err := h.repo.GitRepo.Worktree() - if err != nil { - h.t.Fatal(err) - } - hash, err := w.Commit(msg, &git.CommitOptions{ - Author: &object.Signature{Name: "god"}, - }) - if err != nil { - h.t.Fatal(err) - } - - return hash -} diff --git a/sigcred/credential.go b/sigcred/credential.go deleted file mode 100644 index 8d1a22f..0000000 --- a/sigcred/credential.go +++ /dev/null @@ -1,25 +0,0 @@ -package sigcred - -import "dehub/typeobj" - -// Credential represents a credential which has been attached to a commit which -// hopefully will allow it to be included in the master branch. Exactly one -// field tagged with "type" should be set. -type Credential struct { - PGPSignature *CredentialPGPSignature `type:"pgp_signature"` - - // AccountID specifies the account which generated this Credential. The - // Credentials produced by the Signifier.Sign method do not fill this field - // in. - AccountID string `yaml:"account"` -} - -// MarshalYAML implements the yaml.Marshaler interface. -func (c Credential) MarshalYAML() (interface{}, error) { - return typeobj.MarshalYAML(c) -} - -// UnmarshalYAML implements the yaml.Unmarshaler interface. -func (c *Credential) UnmarshalYAML(unmarshal func(interface{}) error) error { - return typeobj.UnmarshalYAML(c, unmarshal) -} diff --git a/sigcred/pgp.go b/sigcred/pgp.go deleted file mode 100644 index d419178..0000000 --- a/sigcred/pgp.go +++ /dev/null @@ -1,279 +0,0 @@ -package sigcred - -import ( - "bytes" - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/sha256" - "dehub/fs" - "dehub/yamlutil" - "fmt" - "io" - "io/ioutil" - "os/exec" - "path/filepath" - "strings" - "time" - - "golang.org/x/crypto/openpgp/armor" - "golang.org/x/crypto/openpgp/packet" -) - -// CredentialPGPSignature describes a PGP signature which has been used to sign -// a commit. -type CredentialPGPSignature struct { - PubKeyID string `yaml:"pub_key_id"` - Body yamlutil.Blob `yaml:"body"` -} - -type pgpPubKey struct { - pubKey *packet.PublicKey -} - -func newPGPPubKey(r io.Reader) (pgpPubKey, error) { - // TODO support non-armored keys as well - block, err := armor.Decode(r) - if err != nil { - return pgpPubKey{}, fmt.Errorf("could not decode armored PGP public key: %w", err) - } - - pkt, err := packet.Read(block.Body) - if err != nil { - return pgpPubKey{}, fmt.Errorf("could not read PGP public key: %w", err) - } - - pubKey, ok := pkt.(*packet.PublicKey) - if !ok { - return pgpPubKey{}, fmt.Errorf("packet is not a public key, it's a %T", pkt) - } - - return pgpPubKey{pubKey: pubKey}, nil -} - -func (s pgpPubKey) Signed(_ fs.FS, cred Credential) (bool, error) { - if cred.PGPSignature == nil { - return false, nil - } - - return cred.PGPSignature.PubKeyID == s.pubKey.KeyIdString(), nil -} - -func (s pgpPubKey) Verify(_ fs.FS, data []byte, cred Credential) error { - credSig := cred.PGPSignature - if credSig == nil { - return fmt.Errorf("SignifierPGPFile cannot verify %+v", cred) - } - - pkt, err := packet.Read(bytes.NewBuffer(credSig.Body)) - if err != nil { - return fmt.Errorf("could not read signature packet: %w", err) - } - - sigPkt, ok := pkt.(*packet.Signature) - if !ok { - return fmt.Errorf("signature bytes were parsed as a %T, not a signature", pkt) - } - - // The gpg process which is invoked during normal signing automatically - // hashes whatever is piped to it. The VerifySignature method in the openpgp - // package expects you to do it yourself. - h := sigPkt.Hash.New() - h.Write(data) - return s.pubKey.VerifySignature(h, sigPkt) -} - -func (s pgpPubKey) encode() ([]byte, error) { - body := new(bytes.Buffer) - armorEncoder, err := armor.Encode(body, "PGP PUBLIC KEY", nil) - if err != nil { - return nil, fmt.Errorf("error initializing armor encoder: %w", err) - } else if err := s.pubKey.Serialize(armorEncoder); err != nil { - return nil, fmt.Errorf("error encoding public key: %w", err) - } else if err := armorEncoder.Close(); err != nil { - return nil, fmt.Errorf("error closing armor encoder: %w", err) - } - return body.Bytes(), nil -} - -func (s pgpPubKey) asSignfier() (SignifierPGP, error) { - body, err := s.encode() - if err != nil { - return SignifierPGP{}, err - } - - return SignifierPGP{ - Body: string(body), - }, nil -} - -type pgpPrivKey struct { - pgpPubKey - privKey *packet.PrivateKey -} - -// SignifierPGPTmp returns a direct implementation of the SignifierInterface -// which uses a random private key generated in memory, as well as an armored -// version of its public key. -func SignifierPGPTmp(randReader io.Reader) (SignifierInterface, []byte) { - rawPrivKey, err := ecdsa.GenerateKey(elliptic.P521(), randReader) - if err != nil { - panic(err) - } - - privKeyRaw := packet.NewECDSAPrivateKey(time.Now(), rawPrivKey) - privKey := pgpPrivKey{ - pgpPubKey: pgpPubKey{ - pubKey: &privKeyRaw.PublicKey, - }, - privKey: privKeyRaw, - } - - pubKeyBody, err := privKey.pgpPubKey.encode() - if err != nil { - panic(err) - } - return privKey, pubKeyBody -} - -func (s pgpPrivKey) Sign(_ fs.FS, data []byte) (Credential, error) { - h := sha256.New() - h.Write(data) - var sig packet.Signature - sig.Hash = crypto.SHA256 - sig.PubKeyAlgo = s.pubKey.PubKeyAlgo - if err := sig.Sign(h, s.privKey, nil); err != nil { - return Credential{}, fmt.Errorf("failed to sign data: %w", err) - } - - body := new(bytes.Buffer) - if err := sig.Serialize(body); err != nil { - return Credential{}, fmt.Errorf("failed to serialize signature: %w", err) - } - - return Credential{ - PGPSignature: &CredentialPGPSignature{ - PubKeyID: s.pubKey.KeyIdString(), - Body: body.Bytes(), - }, - }, nil -} - -// SignifierPGP describes a pgp public key whose corresponding private key will -// be used as a signing key. -type SignifierPGP struct { - Body string `yaml:"body"` -} - -var _ SignifierInterface = SignifierPGP{} - -func (s SignifierPGP) load() (pgpPubKey, error) { - return newPGPPubKey(strings.NewReader(s.Body)) -} - -// Sign will sign the given arbitrary bytes using the private key corresponding -// to the pgp public key embedded in this Signifier. -func (s SignifierPGP) Sign(fs fs.FS, data []byte) (Credential, error) { - sigPGP, err := s.load() - if err != nil { - return Credential{}, err - } - - stderr := new(bytes.Buffer) - cmd := exec.Command("gpg", - "--openpgp", - "--detach-sign", - "--local-user", sigPGP.pubKey.KeyIdString()) - cmd.Stdin = bytes.NewBuffer(data) - cmd.Stderr = stderr - sig, err := cmd.Output() - if err != nil { - return Credential{}, fmt.Errorf("error signing with gpg (%v): %s", err, stderr.String()) - } - - return Credential{ - PGPSignature: &CredentialPGPSignature{ - PubKeyID: sigPGP.pubKey.KeyIdString(), - Body: sig, - }, - }, nil -} - -// Signed returns true if the private key corresponding to the pgp public key -// embedded in this Signifier was used to produce the given Credential. -func (s SignifierPGP) Signed(fs fs.FS, cred Credential) (bool, error) { - sigPGP, err := s.load() - if err != nil { - return false, err - } - - return sigPGP.Signed(fs, cred) -} - -// Verify asserts that the given signature was produced by this key signing the -// given piece of data. -func (s SignifierPGP) Verify(fs fs.FS, data []byte, cred Credential) error { - sigPGP, err := s.load() - if err != nil { - return err - } - return sigPGP.Verify(fs, data, cred) -} - -// SignifierPGPFile is the same as SignifierPGP, except that the public key is -// found in the repo rather than encoded into the object. -type SignifierPGPFile struct { - Path string `yaml:"path"` -} - -var _ SignifierInterface = SignifierPGPFile{} - -func (s SignifierPGPFile) load(fs fs.FS) (SignifierPGP, error) { - path := filepath.Clean(s.Path) - fr, err := fs.Open(path) - if err != nil { - return SignifierPGP{}, fmt.Errorf("could not open PGP public key file at %q: %w", path, err) - } - defer fr.Close() - - pubKeyB, err := ioutil.ReadAll(fr) - if err != nil { - return SignifierPGP{}, fmt.Errorf("could not read PGP public key from file blob at %q: %w", s.Path, err) - } - - return SignifierPGP{Body: string(pubKeyB)}, nil -} - -// Sign will sign the given arbitrary bytes using the private key corresponding -// to the pgp public key located by this Signifier. -func (s SignifierPGPFile) Sign(fs fs.FS, data []byte) (Credential, error) { - sigPGP, err := s.load(fs) - if err != nil { - return Credential{}, err - } - return sigPGP.Sign(fs, data) -} - -// Signed returns true if the private key corresponding to the pgp public key -// located by this Signifier was used to produce the given Credential. -func (s SignifierPGPFile) Signed(fs fs.FS, cred Credential) (bool, error) { - if cred.PGPSignature == nil { - return false, nil - } - - sigPGP, err := s.load(fs) - if err != nil { - return false, err - } - return sigPGP.Signed(fs, cred) -} - -// Verify asserts that the given signature was produced by this key signing the -// given piece of data. -func (s SignifierPGPFile) Verify(fs fs.FS, data []byte, cred Credential) error { - sigPGP, err := s.load(fs) - if err != nil { - return err - } - return sigPGP.Verify(fs, data, cred) -} diff --git a/sigcred/pgp_test.go b/sigcred/pgp_test.go deleted file mode 100644 index d24edfc..0000000 --- a/sigcred/pgp_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package sigcred - -import ( - "dehub/fs" - "math/rand" - "testing" - "time" -) - -// There are not currently tests for testing pgp signature creation, as they -// require calls out to the gpg executable. Wrapping tests in docker containers -// would make this doable. - -func TestPGPVerification(t *testing.T) { - tests := []struct { - descr string - init func(pubKeyBody []byte) (SignifierInterface, fs.FS) - }{ - { - descr: "SignifierPGP", - init: func(pubKeyBody []byte) (SignifierInterface, fs.FS) { - return SignifierPGP{Body: string(pubKeyBody)}, nil - }, - }, - { - descr: "SignifierPGPFile", - init: func(pubKeyBody []byte) (SignifierInterface, fs.FS) { - pubKeyPath := "some/dir/pubkey.asc" - fs := fs.Stub{pubKeyPath: pubKeyBody} - sigPGPFile := SignifierPGPFile{Path: pubKeyPath} - return sigPGPFile, fs - }, - }, - } - - for _, test := range tests { - t.Run(test.descr, func(t *testing.T) { - seed := time.Now().UnixNano() - t.Logf("seed: %d", seed) - rand := rand.New(rand.NewSource(seed)) - privKey, pubKeyBody := SignifierPGPTmp(rand) - - sig, fs := test.init(pubKeyBody) - data := make([]byte, rand.Intn(1024)) - if _, err := rand.Read(data); err != nil { - t.Fatal(err) - } - - cred, err := privKey.Sign(nil, data) - if err != nil { - t.Fatal(err) - } - - signed, err := sig.Signed(fs, cred) - if err != nil { - t.Fatal(err) - } else if !signed { - t.Fatal("expected signed to be true") - } - - if err := sig.Verify(fs, data, cred); err != nil { - t.Fatal(err) - } - }) - } -} diff --git a/sigcred/sigcred.go b/sigcred/sigcred.go deleted file mode 100644 index e89e601..0000000 --- a/sigcred/sigcred.go +++ /dev/null @@ -1,5 +0,0 @@ -// Package sigcred implements the Signifier and Credential types, which -// interplay together to provide the ability to sign arbitrary blobs of data -// (producing Credentials) and to verify those Credentials within the context of -// a dehub repo. -package sigcred diff --git a/sigcred/signifier.go b/sigcred/signifier.go deleted file mode 100644 index ef99c48..0000000 --- a/sigcred/signifier.go +++ /dev/null @@ -1,50 +0,0 @@ -package sigcred - -import ( - "dehub/fs" - "dehub/typeobj" -) - -// Signifier reprsents a single signing method being defined in the Config. Only -// one field should be set on each Signifier. -type Signifier struct { - PGPPublicKey *SignifierPGP `type:"pgp_public_key"` - PGPPublicKeyFile *SignifierPGPFile `type:"pgp_public_key_file"` -} - -// MarshalYAML implements the yaml.Marshaler interface. -func (s Signifier) MarshalYAML() (interface{}, error) { - return typeobj.MarshalYAML(s) -} - -// UnmarshalYAML implements the yaml.Unmarshaler interface. -func (s *Signifier) UnmarshalYAML(unmarshal func(interface{}) error) error { - return typeobj.UnmarshalYAML(s, unmarshal) -} - -// Interface returns the SignifierInterface instance encapsulated by this -// Signifier object. -func (s Signifier) Interface() (SignifierInterface, error) { - el, _, err := typeobj.Element(s) - if err != nil { - return nil, err - } - return el.(SignifierInterface), nil -} - -// SignifierInterface describes the methods that all Signifiers must implement. -type SignifierInterface interface { - // Sign returns a Credential containing a signature of the given data. - // - // tree can be used to find the Signifier at a particular snapshot. - Sign(fs fs.FS, data []byte) (Credential, error) - - // Signed returns true if the Signifier was used to sign the Credential. - Signed(fs fs.FS, cred Credential) (bool, error) - - // Verify asserts that the Signifier produced the given Credential for the - // given data set, or returns an error. - // - // tree can be used to find the Signifier at a particular snapshot. - Verify(fs fs.FS, data []byte, cred Credential) error -} diff --git a/typeobj/typeobj.go b/typeobj/typeobj.go deleted file mode 100644 index 6b6044d..0000000 --- a/typeobj/typeobj.go +++ /dev/null @@ -1,158 +0,0 @@ -// Package typeobj implements a set of utility functions intended to be used on -// union structs whose fields are tagged with the "type" tag and which expect -// only one of the fields to be set. For example: -// -// type OuterType struct { -// A *InnerTypeA `type:"a"` -// B *InnerTypeB `type:"b"` -// C *InnerTypeC `type:"c"` -// } -// -package typeobj - -import ( - "errors" - "fmt" - "reflect" -) - -// UnmarshalYAML is intended to be used within the UnmarshalYAML method of a -// union struct. It will use the given input data's "type" field and match that -// to the struct field tagged with that value. it will then unmarshal the input -// data into that inner field. -func UnmarshalYAML(i interface{}, unmarshal func(interface{}) error) error { - val := reflect.Indirect(reflect.ValueOf(i)) - if !val.CanSet() { - return fmt.Errorf("cannot unmarshal into value of type %T", i) - } - - // unmarshal in all non-typeobj fields. construct a type which wraps the - // given one, hiding its UnmarshalYAML method (if it has one), and unmarshal - // onto that directly. The "type" field is also unmarshaled at this stage. - valWrap := reflect.New(reflect.StructOf([]reflect.StructField{ - reflect.StructField{ - Name: "Type", - Type: typeOfString, - Tag: `yaml:"type"`, - }, - { - Name: "Val", - Type: val.Type(), - Tag: `yaml:",inline"`, - }, - })) - if err := unmarshal(valWrap.Interface()); err != nil { - return err - } - typeVal := valWrap.Elem().Field(0).String() - val.Set(valWrap.Elem().Field(1)) - - typ := val.Type() - for i := 0; i < val.NumField(); i++ { - fieldVal, fieldTyp := val.Field(i), typ.Field(i) - if fieldTyp.Tag.Get("type") != typeVal { - continue - } - - var valInto interface{} - if fieldVal.Kind() == reflect.Ptr { - newFieldVal := reflect.New(fieldTyp.Type.Elem()) - fieldVal.Set(newFieldVal) - valInto = newFieldVal.Interface() - } else { - valInto = fieldVal.Addr().Interface() - } - return unmarshal(valInto) - } - - return fmt.Errorf("invalid type value %q", typeVal) -} - -// val should be of kind struct -func element(val reflect.Value) (reflect.Value, string, []int, error) { - typ := val.Type() - numFields := val.NumField() - - var fieldVal reflect.Value - var typeTag string - nonTypeFields := make([]int, 0, numFields) - for i := 0; i < numFields; i++ { - innerFieldVal := val.Field(i) - innerTypeTag := typ.Field(i).Tag.Get("type") - if innerTypeTag == "" { - nonTypeFields = append(nonTypeFields, i) - } else if innerFieldVal.IsZero() { - continue - } else { - fieldVal = innerFieldVal - typeTag = innerTypeTag - } - } - - if fieldVal.IsZero() { - return reflect.Value{}, "", nil, errors.New(`no non-zero fields tagged with "type"`) - } - return fieldVal, typeTag, nonTypeFields, nil -} - -// Element returns the value of the first non-zero field tagged with "type", as -// well as the value of the "type" tag. -func Element(i interface{}) (interface{}, string, error) { - val := reflect.Indirect(reflect.ValueOf(i)) - fieldVal, tag, _, err := element(val) - if err != nil { - return fieldVal, tag, err - } - return fieldVal.Interface(), tag, nil -} - -var typeOfString = reflect.TypeOf("string") - -// MarshalYAML is intended to be used within the MarshalYAML method of a union -// struct. It will find the first field of the given struct which has a "type" -// tag and is non-zero. It will then marshal that field's value, inlining an -// extra YAML field "type" whose value is the value of the "type" tag on the -// struct field, and return that. -func MarshalYAML(i interface{}) (interface{}, error) { - val := reflect.Indirect(reflect.ValueOf(i)) - typ := val.Type() - fieldVal, typeTag, nonTypeFields, err := element(val) - if err != nil { - return nil, err - } - - fieldVal = reflect.Indirect(fieldVal) - if fieldVal.Kind() != reflect.Struct { - return nil, fmt.Errorf("cannot marshal non-struct type %T", fieldVal.Interface()) - } - - structFields := make([]reflect.StructField, 0, len(nonTypeFields)+2) - structFields = append(structFields, - reflect.StructField{ - Name: "Type", - Type: typeOfString, - Tag: `yaml:"type"`, - }, - reflect.StructField{ - Name: "Val", - Type: fieldVal.Type(), - Tag: `yaml:",inline"`, - }, - ) - - nonTypeFieldVals := make([]reflect.Value, len(nonTypeFields)) - for i, fieldIndex := range nonTypeFields { - fieldVal, fieldType := val.Field(fieldIndex), typ.Field(fieldIndex) - structFields = append(structFields, fieldType) - nonTypeFieldVals[i] = fieldVal - } - - outVal := reflect.New(reflect.StructOf(structFields)) - outVal.Elem().Field(0).Set(reflect.ValueOf(typeTag)) - outVal.Elem().Field(1).Set(fieldVal) - for i, fieldVal := range nonTypeFieldVals { - outVal.Elem().Field(2 + i).Set(fieldVal) - } - - return outVal.Interface(), nil -} diff --git a/typeobj/typeobj_test.go b/typeobj/typeobj_test.go deleted file mode 100644 index 0939578..0000000 --- a/typeobj/typeobj_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package typeobj - -import ( - "reflect" - "testing" - - "github.com/davecgh/go-spew/spew" - "gopkg.in/yaml.v2" -) - -type foo struct { - A int `yaml:"a"` -} - -type bar struct { - B int `yaml:"b"` -} - -type outer struct { - Foo foo `type:"foo"` - Bar *bar `type:"bar"` - - Other string `yaml:"other_field,omitempty"` -} - -func (o outer) MarshalYAML() (interface{}, error) { - return MarshalYAML(o) -} - -func (o *outer) UnmarshalYAML(unmarshal func(interface{}) error) error { - return UnmarshalYAML(o, unmarshal) -} - -func TestTypeObj(t *testing.T) { - - type test struct { - descr string - str string - err bool - other string - - obj outer - typeTag string - elem interface{} - } - - tests := []test{ - { - descr: "no type set", - str: `{}`, - err: true, - }, - { - descr: "unknown type set", - str: "type: baz", - err: true, - }, - { - descr: "foo set", - str: "type: foo\na: 1\n", - obj: outer{Foo: foo{A: 1}}, - typeTag: "foo", - elem: foo{A: 1}, - }, - { - descr: "bar set", - str: "type: bar\nb: 1\n", - obj: outer{Bar: &bar{B: 1}}, - typeTag: "bar", - elem: &bar{B: 1}, - }, - { - descr: "foo and other_field set", - str: "type: foo\na: 1\nother_field: aaa\n", - obj: outer{Foo: foo{A: 1}, Other: "aaa"}, - typeTag: "foo", - elem: foo{A: 1}, - }, - } - - for _, test := range tests { - t.Run(test.descr, func(t *testing.T) { - var o outer - err := yaml.Unmarshal([]byte(test.str), &o) - if test.err && err != nil { - return - } else if test.err && err == nil { - t.Fatal("expected error when unmarshaling but there was none") - } else if !test.err && err != nil { - t.Fatalf("unmarshaling %q returned unexpected error: %v", test.str, err) - } - - if !reflect.DeepEqual(o, test.obj) { - t.Fatalf("test expected value:\n%s\nbut got value:\n%s", spew.Sprint(test.obj), spew.Sprint(o)) - } - - elem, typeTag, err := Element(o) - if err != nil { - t.Fatalf("error when calling Element(%s): %v", spew.Sprint(o), err) - } else if !reflect.DeepEqual(elem, test.elem) { - t.Fatalf("test expected elem value:\n%s\nbut got value:\n%s", spew.Sprint(test.elem), spew.Sprint(elem)) - } else if typeTag != test.typeTag { - t.Fatalf("test expected typeTag value %q but got %q", test.typeTag, typeTag) - } - - b, err := yaml.Marshal(o) - if err != nil { - t.Fatalf("error marshaling %s: %v", spew.Sprint(o), err) - } else if test.str != string(b) { - t.Fatalf("test expected to marshal to %q, but instead marshaled to %q", test.str, b) - } - }) - } -} diff --git a/yamlutil/yamlutil.go b/yamlutil/yamlutil.go deleted file mode 100644 index f4ef7b4..0000000 --- a/yamlutil/yamlutil.go +++ /dev/null @@ -1,32 +0,0 @@ -// Package yamlutil contains utility types which are useful for dealing with the -// yaml package. -package yamlutil - -import ( - "encoding/base64" -) - -// Blob encodes and decodes a byte slice as a standard base-64 encoded yaml -// string. -type Blob []byte - -// MarshalYAML implements the yaml.Marshaler interface. -func (b Blob) MarshalYAML() (interface{}, error) { - return base64.StdEncoding.EncodeToString([]byte(b)), nil -} - -// UnmarshalYAML implements the yaml.Unmarshaler interface. -func (b *Blob) UnmarshalYAML(unmarshal func(interface{}) error) error { - var b64 string - if err := unmarshal(&b64); err != nil { - return err - } - - b64Dec, err := base64.StdEncoding.DecodeString(b64) - if err != nil { - return err - } - - *b = b64Dec - return nil -} diff --git a/yamlutil/yamlutil_test.go b/yamlutil/yamlutil_test.go deleted file mode 100644 index 4f4854a..0000000 --- a/yamlutil/yamlutil_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package yamlutil - -import ( - "bytes" - "testing" - - yaml "gopkg.in/yaml.v2" -) - -func TestBlob(t *testing.T) { - testCases := []struct { - descr string - in Blob - exp string - }{ - { - descr: "empty", - in: Blob(""), - exp: `""`, - }, - { - descr: "zero", - in: Blob{0}, - exp: "AA==", - }, - { - descr: "zeros", - in: Blob{0, 0, 0}, - exp: "AAAA", - }, - { - descr: "foo", - in: Blob("foo"), - exp: "Zm9v", - }, - } - - for _, test := range testCases { - t.Run(test.descr, func(t *testing.T) { - out, err := yaml.Marshal(test.in) - if err != nil { - t.Fatalf("error marshaling %q: %v", test.in, err) - } else if test.exp+"\n" != string(out) { - t.Fatalf("marshal exp:%q got:%q", test.exp+"\n", out) - } - - var blob Blob - if err := yaml.Unmarshal(out, &blob); err != nil { - t.Fatalf("error unmarshaling %q: %v", out, err) - } else if !bytes.Equal([]byte(blob), []byte(test.in)) { - t.Fatalf("unmarshal exp:%q got:%q", test.in, blob) - } - }) - } -}