aboutsummaryrefslogtreecommitdiff
path: root/file.go
diff options
context:
space:
mode:
Diffstat (limited to 'file.go')
-rw-r--r--file.go215
1 files changed, 215 insertions, 0 deletions
diff --git a/file.go b/file.go
new file mode 100644
index 0000000..b8a5630
--- /dev/null
+++ b/file.go
@@ -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
+}