|
| 1 | +package setup |
| 2 | + |
| 3 | +import ( |
| 4 | + "errors" |
| 5 | + "fmt" |
| 6 | + "maps" |
| 7 | + "slices" |
| 8 | + "strings" |
| 9 | + |
| 10 | + "github.com/canonical/chisel/internal/strdist" |
| 11 | +) |
| 12 | + |
| 13 | +type segmentSlice struct { |
| 14 | + Slice *Slice |
| 15 | + // PathInfo is kept here as an optimzation to avoid lookups on |
| 16 | + // Slice.Contents for every slice. |
| 17 | + PathInfo PathInfo |
| 18 | + // WholePath is used to simplify both error reporting and matching against |
| 19 | + // paths with "**"; both of which require reconstructing the whole path. |
| 20 | + WholePath string |
| 21 | +} |
| 22 | + |
| 23 | +type segment struct { |
| 24 | + Text string |
| 25 | + // HasGlob is set when the path contains "*" or "?" or "**". |
| 26 | + HasGlob bool |
| 27 | + // HasDoubleGlob is set when the path contains "**". |
| 28 | + HasDoubleGlob bool |
| 29 | +} |
| 30 | + |
| 31 | +type node struct { |
| 32 | + Segment segment |
| 33 | + Slices []segmentSlice |
| 34 | + Children map[string]*node |
| 35 | +} |
| 36 | + |
| 37 | +// pathConflictTree uses a custom trie to find conflicts that might arise from |
| 38 | +// extracting different paths into the same root directory. |
| 39 | +// |
| 40 | +// It optimizes conflict resolution by calling strdist.GlobPath only when |
| 41 | +// strictly necessary and by passing it less data to compare. It relies on the |
| 42 | +// fact that real chisel releases most paths often share a very long prefix |
| 43 | +// that does not need to be compared each time. Additionally, our grammar is |
| 44 | +// very restrictive (only "*", "?" and "**") meaning that unless "**" is used, |
| 45 | +// any symbol can only match until a "/" is found. |
| 46 | +// |
| 47 | +// Because of the above, this algorithms splits paths into segments that are |
| 48 | +// delimited by "/". When inserting a path, each segment is compared at most |
| 49 | +// once with the path independently of how many paths there are in the release. |
| 50 | +// Lastly, when looking for conflicts, if the segments do not contain "**" then |
| 51 | +// instead of comparing the whole path we can compare only the segment. |
| 52 | +type pathConflictTree struct { |
| 53 | + Root *node |
| 54 | + PathToSlices map[string][]*Slice |
| 55 | +} |
| 56 | + |
| 57 | +func newConflictTree(pathToSlices map[string][]*Slice) pathConflictTree { |
| 58 | + root := &node{ |
| 59 | + Segment: segment{"/", false, false}, |
| 60 | + Children: map[string]*node{}, |
| 61 | + } |
| 62 | + return pathConflictTree{Root: root, PathToSlices: pathToSlices} |
| 63 | +} |
| 64 | + |
| 65 | +func (g *pathConflictTree) HasConflict() error { |
| 66 | + for path, slices := range g.PathToSlices { |
| 67 | + var oldInfos []segmentSlice |
| 68 | + for _, oldSlice := range slices { |
| 69 | + oldInfos = append(oldInfos, segmentSlice{oldSlice, oldSlice.Contents[path], path}) |
| 70 | + } |
| 71 | + segments, err := pathToSegments(path) |
| 72 | + if err != nil { |
| 73 | + return err |
| 74 | + } |
| 75 | + err = g.pathHasConflict(path, segments, oldInfos) |
| 76 | + if err != nil { |
| 77 | + return err |
| 78 | + } |
| 79 | + g.insertSegments(segments, oldInfos) |
| 80 | + } |
| 81 | + return nil |
| 82 | +} |
| 83 | + |
| 84 | +func (g *pathConflictTree) pathHasConflict(oldPath string, oldSegments []segment, oldInfos []segmentSlice) error { |
| 85 | + conflictErrMsg := func(oldInfo, newInfo *segmentSlice) error { |
| 86 | + oldSlice, oldPath := oldInfo.Slice, oldInfo.WholePath |
| 87 | + newSlice, newPath := newInfo.Slice, newInfo.WholePath |
| 88 | + if (oldSlice.Package > newSlice.Package) || (oldSlice.Package == newSlice.Package && oldSlice.Name > newSlice.Name) || |
| 89 | + (oldSlice.Package == newSlice.Package && oldSlice.Name == newSlice.Name && oldPath > newPath) { |
| 90 | + oldSlice, newSlice = newSlice, oldSlice |
| 91 | + oldPath, newPath = newPath, oldPath |
| 92 | + } |
| 93 | + return fmt.Errorf("slices %s and %s conflict on %s and %s", oldSlice, newSlice, oldPath, newPath) |
| 94 | + } |
| 95 | + |
| 96 | + var currentQueue []*node |
| 97 | + var nextQueue []*node |
| 98 | + |
| 99 | + // Skip "/". |
| 100 | + currentQueue = slices.Collect(maps.Values(g.Root.Children)) |
| 101 | + oldSegments = oldSegments[1:] |
| 102 | + |
| 103 | + for len(currentQueue) > 0 { |
| 104 | + oldSegment := oldSegments[0] |
| 105 | + for _, newNode := range currentQueue { |
| 106 | + newNodeLoop: |
| 107 | + for _, oldSegmentInfo := range oldInfos { |
| 108 | + oldSlice := oldSegmentInfo.Slice |
| 109 | + oldPathInfo := oldSegmentInfo.PathInfo |
| 110 | + for _, newSegmentInfo := range newNode.Slices { |
| 111 | + newSlice := newSegmentInfo.Slice |
| 112 | + newPathInfo := newSegmentInfo.PathInfo |
| 113 | + newSegment := newNode.Segment |
| 114 | + |
| 115 | + // If slices cannot conflict then skip the more expensive |
| 116 | + // checks. |
| 117 | + if (oldPathInfo.Kind == GlobPath || oldPathInfo.Kind == CopyPath) && (newPathInfo.Kind == GlobPath || newPathInfo.Kind == CopyPath) { |
| 118 | + if newSlice.Package == oldSlice.Package { |
| 119 | + // If content is **extracted** from the same |
| 120 | + // package, it will necessarily be the same. |
| 121 | + continue |
| 122 | + } |
| 123 | + } |
| 124 | + |
| 125 | + if newSegment.HasDoubleGlob || oldSegment.HasDoubleGlob { |
| 126 | + // Case 1: One of the strings has a double glob, we |
| 127 | + // need to check the whole remaining path against |
| 128 | + // each other. |
| 129 | + if strdist.GlobPath(oldSegmentInfo.WholePath, newSegmentInfo.WholePath) { |
| 130 | + return conflictErrMsg(&oldSegmentInfo, &newSegmentInfo) |
| 131 | + } |
| 132 | + } else if newSegment.HasGlob || oldSegment.HasGlob { |
| 133 | + // Case 2: Either segment has a single glob (* or ?). |
| 134 | + // We only need to check the segment. |
| 135 | + if strdist.GlobPath(oldSegment.Text, newSegment.Text) { |
| 136 | + // Only when we get to leaf (i.e. no children, can |
| 137 | + // we have a conflict). |
| 138 | + if len(newNode.Children) == 0 { |
| 139 | + if len(oldSegments) == 1 { |
| 140 | + // If we are at the terminal node of both paths we found a conflict. |
| 141 | + return conflictErrMsg(&oldSegmentInfo, &newSegmentInfo) |
| 142 | + } else { |
| 143 | + // If oldPath is not yet finished we will keep comparing it against |
| 144 | + // this segment. Example: ["/", "a/", "*", ""] and ["/", "a/", ""]; |
| 145 | + // the segments ["*", ""] match [""]. |
| 146 | + nextQueue = append(nextQueue, newNode) |
| 147 | + } |
| 148 | + } |
| 149 | + for _, child := range newNode.Children { |
| 150 | + nextQueue = append(nextQueue, child) |
| 151 | + } |
| 152 | + break newNodeLoop |
| 153 | + } else { |
| 154 | + // Once GlobPath returns false there cannot be a |
| 155 | + // conflict between oldPath and newPath, we can |
| 156 | + // break here. |
| 157 | + break newNodeLoop |
| 158 | + } |
| 159 | + } else { |
| 160 | + // Case 3: No globs, we can compare the strings directly. |
| 161 | + if oldSegment.Text == newSegment.Text { |
| 162 | + if len(newNode.Children) == 0 && len(oldSegments) == 1 { |
| 163 | + // If these are both terminal nodes, conflict found. |
| 164 | + return conflictErrMsg(&oldSegmentInfo, &newSegmentInfo) |
| 165 | + } |
| 166 | + for _, child := range newNode.Children { |
| 167 | + nextQueue = append(nextQueue, child) |
| 168 | + } |
| 169 | + break newNodeLoop |
| 170 | + } |
| 171 | + } |
| 172 | + } |
| 173 | + } |
| 174 | + } |
| 175 | + currentQueue, nextQueue = nextQueue, currentQueue |
| 176 | + nextQueue = nextQueue[0:0] |
| 177 | + |
| 178 | + if len(oldSegments) > 1 { |
| 179 | + // If the segment is a termination node keep it. See example in case 2. |
| 180 | + oldSegments = oldSegments[1:] |
| 181 | + } |
| 182 | + } |
| 183 | + |
| 184 | + return nil |
| 185 | +} |
| 186 | + |
| 187 | +// insertSegments inserts the path's segments blindly in the graph without |
| 188 | +// looking at conflicts. |
| 189 | +func (g *pathConflictTree) insertSegments(segments []segment, infos []segmentSlice) { |
| 190 | + parent := g.Root |
| 191 | + // Skip "/". |
| 192 | + segments = segments[1:] |
| 193 | + |
| 194 | + for _, segment := range segments { |
| 195 | + current, ok := parent.Children[segment.Text] |
| 196 | + if !ok { |
| 197 | + current = &node{ |
| 198 | + Segment: segment, |
| 199 | + Children: map[string]*node{}, |
| 200 | + } |
| 201 | + } |
| 202 | + current.Slices = append(current.Slices, infos...) |
| 203 | + parent.Children[segment.Text] = current |
| 204 | + parent = current |
| 205 | + } |
| 206 | +} |
| 207 | + |
| 208 | +// pathToSegments returns the list of segments that compose the path plus the |
| 209 | +// empty segment "" for explicit termination in the trie. |
| 210 | +func pathToSegments(path string) ([]segment, error) { |
| 211 | + if path[0] != '/' { |
| 212 | + return nil, errors.New("internal error: path does not start with '/'") |
| 213 | + } |
| 214 | + segments := []segment{segment{"/", false, false}} |
| 215 | + path = path[1:] |
| 216 | + for { |
| 217 | + end, singleGlob, doubleGlob := segmentEnd(path) |
| 218 | + segment := segment{ |
| 219 | + Text: path[:end+1], |
| 220 | + HasGlob: singleGlob, |
| 221 | + HasDoubleGlob: doubleGlob, |
| 222 | + } |
| 223 | + segments = append(segments, segment) |
| 224 | + path = path[end+1:] |
| 225 | + if segment.Text == "" { |
| 226 | + break |
| 227 | + } |
| 228 | + } |
| 229 | + return segments, nil |
| 230 | +} |
| 231 | + |
| 232 | +// segmentEnd finds the end of a segment according to the following rules: |
| 233 | +// - If s contains "**" then segment = s. |
| 234 | +// - Else if the s contains "/" then segment will finish at the first "/" |
| 235 | +// found. |
| 236 | +// - Else segment = s. |
| 237 | +// |
| 238 | +// hasGlob is set to true if "*", "?" or "**" is found in the segment. |
| 239 | +// hasDoubleGlob is set to true if "**" is found in the segment. |
| 240 | +func segmentEnd(s string) (end int, hasGlob bool, hasDoubleGlob bool) { |
| 241 | + end = strings.IndexAny(s, "*?/") |
| 242 | + if end == -1 { |
| 243 | + end = len(s) - 1 |
| 244 | + } else if s[end] == '*' || s[end] == '?' { |
| 245 | + hasGlob = true |
| 246 | + slash := strings.IndexRune(s[end:], '/') |
| 247 | + if slash != -1 { |
| 248 | + end = end + slash |
| 249 | + } else { |
| 250 | + end = len(s) - 1 |
| 251 | + } |
| 252 | + hasDoubleGlob = strings.Contains(s[:end+1], "**") |
| 253 | + if hasDoubleGlob { |
| 254 | + end = len(s) - 1 |
| 255 | + } |
| 256 | + } |
| 257 | + return end, hasGlob, hasDoubleGlob |
| 258 | +} |
0 commit comments