| // Copyright 2018 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package common |
| |
| import ( |
| "github.com/google/cel-go/common/runes" |
| |
| exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" |
| ) |
| |
| // Source interface for filter source contents. |
| type Source interface { |
| // Content returns the source content represented as a string. |
| // Examples contents are the single file contents, textbox field, |
| // or url parameter. |
| Content() string |
| |
| // Description gives a brief description of the source. |
| // Example descriptions are a file name or ui element. |
| Description() string |
| |
| // LineOffsets gives the character offsets at which lines occur. |
| // The zero-th entry should refer to the break between the first |
| // and second line, or EOF if there is only one line of source. |
| LineOffsets() []int32 |
| |
| // LocationOffset translates a Location to an offset. |
| // Given the line and column of the Location returns the |
| // Location's character offset in the Source, and a bool |
| // indicating whether the Location was found. |
| LocationOffset(location Location) (int32, bool) |
| |
| // OffsetLocation translates a character offset to a Location, or |
| // false if the conversion was not feasible. |
| OffsetLocation(offset int32) (Location, bool) |
| |
| // NewLocation takes an input line and column and produces a Location. |
| // The default behavior is to treat the line and column as absolute, |
| // but concrete derivations may use this method to convert a relative |
| // line and column position into an absolute location. |
| NewLocation(line, col int) Location |
| |
| // Snippet returns a line of content and whether the line was found. |
| Snippet(line int) (string, bool) |
| } |
| |
| // The sourceImpl type implementation of the Source interface. |
| type sourceImpl struct { |
| runes.Buffer |
| description string |
| lineOffsets []int32 |
| } |
| |
| var _ runes.Buffer = &sourceImpl{} |
| |
| // TODO(jimlarson) "Character offsets" should index the code points |
| // within the UTF-8 encoded string. It currently indexes bytes. |
| // Can be accomplished by using rune[] instead of string for contents. |
| |
| // NewTextSource creates a new Source from the input text string. |
| func NewTextSource(text string) Source { |
| return NewStringSource(text, "<input>") |
| } |
| |
| // NewTextSourceWithLimit creates a new Source from the input text string while |
| // enforcing a maximum code point count when needed. |
| func NewTextSourceWithLimit(text string, limit int) (Source, error) { |
| return NewStringSourceWithLimit(text, "<input>", limit) |
| } |
| |
| // NewStringSource creates a new Source from the given contents and description. |
| func NewStringSource(contents string, description string) Source { |
| // Compute line offsets up front as they are referred to frequently. |
| buf, offs := runes.NewBufferAndLineOffsets(contents) |
| return &sourceImpl{ |
| Buffer: buf, |
| description: description, |
| lineOffsets: offs, |
| } |
| } |
| |
| // NewStringSourceWithLimit creates a new Source from the given contents and |
| // description while enforcing a maximum code point count when needed. |
| func NewStringSourceWithLimit(contents string, description string, limit int) (Source, error) { |
| if limit < 0 || len(contents) <= limit { |
| return NewStringSource(contents, description), nil |
| } |
| buf, offs, err := runes.NewBufferAndLineOffsetsWithLimit(contents, limit) |
| if err != nil { |
| return nil, err |
| } |
| return &sourceImpl{ |
| Buffer: buf, |
| description: description, |
| lineOffsets: offs, |
| }, nil |
| } |
| |
| // NewInfoSource creates a new Source from a SourceInfo. |
| func NewInfoSource(info *exprpb.SourceInfo) Source { |
| return &sourceImpl{ |
| Buffer: runes.NewBuffer(""), |
| description: info.GetLocation(), |
| lineOffsets: info.GetLineOffsets(), |
| } |
| } |
| |
| // Content implements the Source interface method. |
| func (s *sourceImpl) Content() string { |
| return s.Slice(0, s.Len()) |
| } |
| |
| // Description implements the Source interface method. |
| func (s *sourceImpl) Description() string { |
| return s.description |
| } |
| |
| // LineOffsets implements the Source interface method. |
| func (s *sourceImpl) LineOffsets() []int32 { |
| return s.lineOffsets |
| } |
| |
| // LocationOffset implements the Source interface method. |
| func (s *sourceImpl) LocationOffset(location Location) (int32, bool) { |
| if lineOffset, found := s.findLineOffset(location.Line()); found { |
| return lineOffset + int32(location.Column()), true |
| } |
| return -1, false |
| } |
| |
| // NewLocation implements the Source interface method. |
| func (s *sourceImpl) NewLocation(line, col int) Location { |
| return NewLocation(line, col) |
| } |
| |
| // OffsetLocation implements the Source interface method. |
| func (s *sourceImpl) OffsetLocation(offset int32) (Location, bool) { |
| line, lineOffset := s.findLine(offset) |
| return NewLocation(int(line), int(offset-lineOffset)), true |
| } |
| |
| // Snippet implements the Source interface method. |
| func (s *sourceImpl) Snippet(line int) (string, bool) { |
| charStart, found := s.findLineOffset(line) |
| if !found || s.Len() == 0 { |
| return "", false |
| } |
| charEnd, found := s.findLineOffset(line + 1) |
| if found { |
| return s.Slice(int(charStart), int(charEnd-1)), true |
| } |
| return s.Slice(int(charStart), s.Len()), true |
| } |
| |
| // findLineOffset returns the offset where the (1-indexed) line begins, |
| // or false if line doesn't exist. |
| func (s *sourceImpl) findLineOffset(line int) (int32, bool) { |
| if line == 1 { |
| return 0, true |
| } |
| if line > 1 && line <= int(len(s.lineOffsets)) { |
| offset := s.lineOffsets[line-2] |
| return offset, true |
| } |
| return -1, false |
| } |
| |
| // findLine finds the line that contains the given character offset and |
| // returns the line number and offset of the beginning of that line. |
| // Note that the last line is treated as if it contains all offsets |
| // beyond the end of the actual source. |
| func (s *sourceImpl) findLine(characterOffset int32) (int32, int32) { |
| var line int32 = 1 |
| for _, lineOffset := range s.lineOffsets { |
| if lineOffset > characterOffset { |
| break |
| } |
| line++ |
| } |
| if line == 1 { |
| return line, 0 |
| } |
| return line, s.lineOffsets[line-2] |
| } |