Serge Bazanski | e50ec39 | 2020-06-30 21:41:39 +0200 | [diff] [blame] | 1 | // Copyright 2020 The Monogon Project Authors. |
| 2 | // |
| 3 | // SPDX-License-Identifier: Apache-2.0 |
| 4 | // |
| 5 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | // you may not use this file except in compliance with the License. |
| 7 | // You may obtain a copy of the License at |
| 8 | // |
| 9 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | // |
| 11 | // Unless required by applicable law or agreed to in writing, software |
| 12 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | // See the License for the specific language governing permissions and |
| 15 | // limitations under the License. |
| 16 | |
| 17 | package declarative |
| 18 | |
| 19 | import ( |
| 20 | "fmt" |
| 21 | "reflect" |
| 22 | "strings" |
| 23 | ) |
| 24 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 25 | // Directory represents the intent of existence of a directory in a |
| 26 | // hierarchical filesystem (simplified to a tree). This structure can be |
| 27 | // embedded and still be interpreted as a Directory for purposes of use within |
| 28 | // this library. Any inner fields of such an embedding structure that are in |
| 29 | // turn (embedded) Directories or files will be treated as children in the |
| 30 | // intent expressed by this Directory. All contained directory fields must have |
| 31 | // a `dir:"name"` struct tag that names them, and all contained file fields |
| 32 | // must have a `file:"name"` struct tag. |
Serge Bazanski | e50ec39 | 2020-06-30 21:41:39 +0200 | [diff] [blame] | 33 | // |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 34 | // Creation and management of the directory at runtime is left to the |
| 35 | // implementing code. However, the DirectoryPlacement implementation (set as |
| 36 | // the directory is placed onto a backing store) facilitates this management |
| 37 | // (by exposing methods that mutate the backing store). |
Serge Bazanski | e50ec39 | 2020-06-30 21:41:39 +0200 | [diff] [blame] | 38 | type Directory struct { |
| 39 | DirectoryPlacement |
| 40 | } |
| 41 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 42 | // File represents the intent of existence of a file. files are usually child |
| 43 | // structures in types that embed Directory. File can also be embedded in |
| 44 | // another structure, and this embedding type will still be interpreted as a |
| 45 | // File for purposes of use within this library. |
Serge Bazanski | e50ec39 | 2020-06-30 21:41:39 +0200 | [diff] [blame] | 46 | // |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 47 | // As with Directory, the runtime management of a File in a backing store is |
| 48 | // left to the implementing code, and the embedded FilePlacement interface |
| 49 | // facilitates access to the backing store. |
Serge Bazanski | e50ec39 | 2020-06-30 21:41:39 +0200 | [diff] [blame] | 50 | type File struct { |
| 51 | FilePlacement |
| 52 | } |
| 53 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 54 | // unpackDirectory takes a pointer to Directory or a pointer to a structure |
| 55 | // embedding Directory, and returns a reflection Value that refers to the |
| 56 | // passed structure itself (not its pointer) and a plain Go pointer to the |
Serge Bazanski | e50ec39 | 2020-06-30 21:41:39 +0200 | [diff] [blame] | 57 | // (embedded) Directory. |
| 58 | func unpackDirectory(d interface{}) (*reflect.Value, *Directory, error) { |
| 59 | td := reflect.TypeOf(d) |
| 60 | if td.Kind() != reflect.Ptr { |
| 61 | return nil, nil, fmt.Errorf("wanted a pointer, got %v", td.Kind()) |
| 62 | } |
| 63 | |
| 64 | var dir *Directory |
| 65 | id := reflect.ValueOf(d).Elem() |
| 66 | tid := id.Type() |
| 67 | switch { |
| 68 | case tid.Name() == reflect.TypeOf(Directory{}).Name(): |
| 69 | dir = id.Addr().Interface().(*Directory) |
| 70 | case id.FieldByName("Directory").IsValid(): |
| 71 | dir = id.FieldByName("Directory").Addr().Interface().(*Directory) |
| 72 | default: |
| 73 | return nil, nil, fmt.Errorf("not a Directory or embedding Directory (%v)", id.Type().String()) |
| 74 | } |
| 75 | return &id, dir, nil |
| 76 | } |
| 77 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 78 | // unpackFile takes a pointer to a File or a pointer to a structure embedding |
| 79 | // File, and returns a reflection Value that refers to the passed structure |
| 80 | // itself (not its pointer) and a plain Go pointer to the (embedded) File. |
Serge Bazanski | e50ec39 | 2020-06-30 21:41:39 +0200 | [diff] [blame] | 81 | func unpackFile(f interface{}) (*reflect.Value, *File, error) { |
| 82 | tf := reflect.TypeOf(f) |
| 83 | if tf.Kind() != reflect.Ptr { |
| 84 | return nil, nil, fmt.Errorf("wanted a pointer, got %v", tf.Kind()) |
| 85 | } |
| 86 | |
| 87 | var fil *File |
| 88 | id := reflect.ValueOf(f).Elem() |
| 89 | tid := id.Type() |
| 90 | switch { |
| 91 | case tid.Name() == reflect.TypeOf(File{}).Name(): |
| 92 | fil = id.Addr().Interface().(*File) |
| 93 | case id.FieldByName("File").IsValid(): |
| 94 | fil = id.FieldByName("File").Addr().Interface().(*File) |
| 95 | default: |
| 96 | return nil, nil, fmt.Errorf("not a File or embedding File (%v)", tid.String()) |
| 97 | } |
| 98 | return &id, fil, nil |
| 99 | |
| 100 | } |
| 101 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 102 | // subdirs takes a pointer to a Directory or pointer to a structure embedding |
| 103 | // Directory, and returns a pair of pointers to Directory-like structures |
| 104 | // contained within that directory with corresponding names (based on struct |
| 105 | // tags). |
Serge Bazanski | e50ec39 | 2020-06-30 21:41:39 +0200 | [diff] [blame] | 106 | func subdirs(d interface{}) ([]namedDirectory, error) { |
| 107 | s, _, err := unpackDirectory(d) |
| 108 | if err != nil { |
| 109 | return nil, fmt.Errorf("argument could not be parsed as *Directory: %w", err) |
| 110 | } |
| 111 | |
| 112 | var res []namedDirectory |
| 113 | for i := 0; i < s.NumField(); i++ { |
| 114 | tf := s.Type().Field(i) |
| 115 | dirTag := tf.Tag.Get("dir") |
| 116 | if dirTag == "" { |
| 117 | continue |
| 118 | } |
| 119 | sf := s.Field(i) |
| 120 | res = append(res, namedDirectory{dirTag, sf.Addr().Interface()}) |
| 121 | } |
| 122 | return res, nil |
| 123 | } |
| 124 | |
| 125 | type namedDirectory struct { |
| 126 | name string |
| 127 | directory interface{} |
| 128 | } |
| 129 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 130 | // files takes a pointer to a File or pointer to a structure embedding File, |
| 131 | // and returns a pair of pointers to Directory-like structures contained within |
| 132 | // that directory with corresponding names (based on struct tags). |
Serge Bazanski | e50ec39 | 2020-06-30 21:41:39 +0200 | [diff] [blame] | 133 | func files(d interface{}) ([]namedFile, error) { |
| 134 | s, _, err := unpackDirectory(d) |
| 135 | if err != nil { |
| 136 | return nil, fmt.Errorf("argument could not be parsed as *Directory: %w", err) |
| 137 | } |
| 138 | |
| 139 | var res []namedFile |
| 140 | for i := 0; i < s.NumField(); i++ { |
| 141 | tf := s.Type().Field(i) |
| 142 | fileTag := tf.Tag.Get("file") |
| 143 | if fileTag == "" { |
| 144 | continue |
| 145 | } |
| 146 | _, f, err := unpackFile(s.Field(i).Addr().Interface()) |
| 147 | if err != nil { |
| 148 | return nil, fmt.Errorf("file %q could not be parsed as *File: %w", tf.Name, err) |
| 149 | } |
| 150 | res = append(res, namedFile{fileTag, f}) |
| 151 | } |
| 152 | return res, nil |
| 153 | } |
| 154 | |
| 155 | type namedFile struct { |
| 156 | name string |
| 157 | file *File |
| 158 | } |
| 159 | |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 160 | // Validate checks that a given pointer to a Directory or pointer to a |
| 161 | // structure containing Directory does not contain any programmer errors in its |
| 162 | // definition: |
Serge Bazanski | e50ec39 | 2020-06-30 21:41:39 +0200 | [diff] [blame] | 163 | // - all subdirectories/files must be named |
| 164 | // - all subdirectory/file names within a directory must be unique |
Serge Bazanski | 216fe7b | 2021-05-21 18:36:16 +0200 | [diff] [blame^] | 165 | // - all subdirectory/file names within a directory must not contain the '/' |
| 166 | // character (as it is a common path delimiter) |
Serge Bazanski | e50ec39 | 2020-06-30 21:41:39 +0200 | [diff] [blame] | 167 | func Validate(d interface{}) error { |
| 168 | names := make(map[string]bool) |
| 169 | |
| 170 | subs, err := subdirs(d) |
| 171 | if err != nil { |
| 172 | return fmt.Errorf("could not get subdirectories: %w", err) |
| 173 | } |
| 174 | |
| 175 | for _, nd := range subs { |
| 176 | if nd.name == "" { |
| 177 | return fmt.Errorf("subdirectory with empty name") |
| 178 | } |
| 179 | if strings.Contains(nd.name, "/") { |
| 180 | return fmt.Errorf("subdirectory with invalid path: %q", nd.name) |
| 181 | } |
| 182 | if names[nd.name] { |
| 183 | return fmt.Errorf("subdirectory with duplicate name: %q", nd.name) |
| 184 | } |
| 185 | names[nd.name] = true |
| 186 | |
| 187 | err := Validate(nd.directory) |
| 188 | if err != nil { |
| 189 | return fmt.Errorf("%s: %w", nd.name, err) |
| 190 | } |
| 191 | } |
| 192 | |
| 193 | filelist, err := files(d) |
| 194 | if err != nil { |
| 195 | return fmt.Errorf("could not get files: %w", err) |
| 196 | } |
| 197 | |
| 198 | for _, nf := range filelist { |
| 199 | if nf.name == "" { |
| 200 | return fmt.Errorf("file with empty name") |
| 201 | } |
| 202 | if strings.Contains(nf.name, "/") { |
| 203 | return fmt.Errorf("file with invalid path: %q", nf.name) |
| 204 | } |
| 205 | if names[nf.name] { |
| 206 | return fmt.Errorf("file with duplicate name: %q", nf.name) |
| 207 | } |
| 208 | names[nf.name] = true |
| 209 | } |
| 210 | return nil |
| 211 | } |