diff options
Diffstat (limited to 'file.go')
| -rw-r--r-- | file.go | 215 |
1 files changed, 215 insertions, 0 deletions
@@ -0,0 +1,215 @@ +// SPDX-License-Identifier: GPL-3.0-only +// SPDX-FileCopyrightText: 2025 M. Shulhan <ms@kilabit.info> + +package spdxconv + +import ( + "bytes" + "fmt" + "os" + "regexp" + "slices" +) + +// reLicenseID regex to detect SPDX license identifier with or without +// comment prefix. +var reLicenseID = regexp.MustCompile(`^(//+|#+|/\*+|<!--+)?\s?SPDX-License-Identifier:.*$`) + +type file struct { + path string + + // commentPrefix used as prefix to SPDX identifier. + // The comment prefix is detected automatically from the first N + // lines of file. + commentPrefix string + commentSuffix string + + lines [][]byte + topLines [][]byte + bottomLines [][]byte + + // idxLicenseID index of License-Identifier in the topLines. + idxLicenseID int + + hasSheBang bool +} + +func newFile(path string, maxLine int) (f *file, err error) { + var content []byte + content, err = os.ReadFile(path) + if err != nil { + return nil, err + } + + f = &file{ + path: path, + lines: bytes.Split(content, []byte{'\n'}), + idxLicenseID: -1, + } + nline := len(f.lines) + if nline < maxLine*2 { + f.topLines = f.lines + f.lines = f.lines[nline:] + } else { + f.topLines = f.lines[:maxLine] + f.bottomLines = f.lines[nline-maxLine:] + f.lines = f.lines[maxLine : nline-maxLine] + } + return f, nil +} + +// apply the SPDX identifier to file. +func (f *file) apply(conv *SPDXConv) { + f.detectComment() + f.applyLicenseID(conv) + f.insertEmptyLine() +} + +func (f *file) detectComment() { + if bytes.HasPrefix(f.topLines[0], []byte(`#!`)) { + f.hasSheBang = true + f.commentPrefix = `# ` + return + } + for _, line := range f.topLines { + if bytes.HasPrefix(line, []byte(`#`)) { + f.commentPrefix = `# ` + return + } + if bytes.HasPrefix(line, []byte(`//`)) { + f.commentPrefix = `// ` + return + } + if bytes.HasPrefix(line, []byte(`/*`)) { + f.commentPrefix = `// ` + return + } + if bytes.HasPrefix(line, []byte(`<!--`)) { + f.commentPrefix = `<!-- ` + f.commentSuffix = ` -->` + return + } + } +} + +// applyLicenseID check and insert the SPDX-License-Identifier. +// +// Its detect if SPDX-License-Identifer exist at the top or bottom of +// the file. +// If one found at the top, but not at the first line, or at the +// bottom, move it to the first line, after shebang. +func (f *file) applyLicenseID(conv *SPDXConv) { + var licenseID string + + for _, cml := range conv.cfg.MatchLicense { + for x, line := range f.topLines { + if reLicenseID.Match(line) { + f.idxLicenseID = x + if f.hasSheBang && x == 1 { + return + } + if x == 0 { + return + } + f.topLines = slices.Delete(f.topLines, x, x+1) + f.insertLicenseID(line) + return + } + if cml.rePattern.Match(line) { + licenseID = cml.LicenseIdentifier + if cml.DeleteMatch { + f.topLines = slices.Delete(f.topLines, x, x+1) + } + f.deleteLinePattern(f.topLines[x:], cml.reDeleteLine) + } + } + if licenseID != `` { + break + } + for x, line := range f.bottomLines { + if reLicenseID.Match(line) { + f.bottomLines = slices.Delete(f.bottomLines, x, x+1) + f.insertLicenseID(line) + return + } + if cml.rePattern.Match(line) { + licenseID = cml.LicenseIdentifier + if cml.DeleteMatch { + f.bottomLines = slices.Delete(f.bottomLines, x, x+1) + } + f.deleteLinePattern(f.bottomLines[x:], cml.reDeleteLine) + } + } + if licenseID != `` { + break + } + } + if licenseID == `` { + licenseID = conv.cfg.LicenseIdentifier + } + line := fmt.Sprintf("%sSPDX-License-Identifier: %s%s", + f.commentPrefix, licenseID, f.commentSuffix) + f.insertLicenseID([]byte(line)) +} + +// insertEmptyLine insert empty line after SPDX identifiers or any comments after it. +func (f *file) insertEmptyLine() { + if f.idxLicenseID < 0 || f.commentPrefix == `` { + // No license ID inserted. + return + } + comment := []byte(f.commentPrefix) + comment = comment[:len(comment)-1] // Remove space. + for x, line := range f.topLines[f.idxLicenseID:] { + if bytes.HasPrefix(line, comment) { + continue + } + line = bytes.TrimSpace(line) + if len(line) == 0 { + // There is already empty line. + return + } + f.topLines = slices.Insert(f.topLines, x, []byte{}) + return + } +} + +// insertLicenseID insert the license identifier `line` at the top of the +// file and below the shebang "#!" if its exists. +func (f *file) insertLicenseID(line []byte) { + if f.hasSheBang { + f.topLines = slices.Insert(f.topLines, 1, line) + f.idxLicenseID = 1 + } else { + f.topLines = slices.Insert(f.topLines, 0, line) + f.idxLicenseID = 0 + } +} + +func (f *file) deleteLinePattern(lines [][]byte, reDeleteLine []*regexp.Regexp) { + for _, re := range reDeleteLine { + for x, line := range lines { + if re.Match(line) { + lines = slices.Delete(lines, x, x+1) + break + } + } + } +} + +func (f *file) write() (err error) { + var finfo os.FileInfo + finfo, err = os.Stat(f.path) + if err != nil { + return fmt.Errorf(`write: %w`, err) + } + + lines := slices.Concat(f.topLines, f.lines, f.bottomLines) + content := bytes.Join(lines, []byte{'\n'}) + content = bytes.TrimRight(content, "\n") + err = os.WriteFile(f.path, content, finfo.Mode()) + if err != nil { + return fmt.Errorf(`write: %w`, err) + } + return nil +} |
