blob: 4f9b0878190aed1fce0f4674c6f87620168c03f8 [file] [log] [blame]
Serge Bazanskie50ec392020-06-30 21:41:39 +02001// 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
17package declarative
18
19import (
20 "fmt"
21 "reflect"
22 "strings"
23)
24
Serge Bazanski216fe7b2021-05-21 18:36:16 +020025// 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 Bazanskie50ec392020-06-30 21:41:39 +020033//
Serge Bazanski216fe7b2021-05-21 18:36:16 +020034// 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 Bazanskie50ec392020-06-30 21:41:39 +020038type Directory struct {
39 DirectoryPlacement
40}
41
Serge Bazanski216fe7b2021-05-21 18:36:16 +020042// 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 Bazanskie50ec392020-06-30 21:41:39 +020046//
Serge Bazanski216fe7b2021-05-21 18:36:16 +020047// 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 Bazanskie50ec392020-06-30 21:41:39 +020050type File struct {
51 FilePlacement
52}
53
Serge Bazanski216fe7b2021-05-21 18:36:16 +020054// 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 Bazanskie50ec392020-06-30 21:41:39 +020057// (embedded) Directory.
58func 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 Bazanski216fe7b2021-05-21 18:36:16 +020078// 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 Bazanskie50ec392020-06-30 21:41:39 +020081func 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 Bazanski216fe7b2021-05-21 18:36:16 +0200102// 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 Bazanskie50ec392020-06-30 21:41:39 +0200106func 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
125type namedDirectory struct {
126 name string
127 directory interface{}
128}
129
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200130// 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 Bazanskie50ec392020-06-30 21:41:39 +0200133func 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
155type namedFile struct {
156 name string
157 file *File
158}
159
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200160// 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 Bazanskie50ec392020-06-30 21:41:39 +0200163// - all subdirectories/files must be named
164// - all subdirectory/file names within a directory must be unique
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200165// - all subdirectory/file names within a directory must not contain the '/'
166// character (as it is a common path delimiter)
Serge Bazanskie50ec392020-06-30 21:41:39 +0200167func 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}