├── go.mod ├── .gitignore ├── .github └── workflows │ └── build_and_lint.yml ├── README.md ├── LICENSE ├── org.gnome.gnome-power-statistics.metainfo.xml └── appstreamlint.go /go.mod: -------------------------------------------------------------------------------- 1 | module appstreamlint 2 | 3 | go 1.22.1 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | appstreamlint 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | # Output of the go coverage tool, specifically when used with LiteIDE 16 | *.out 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | # Go workspace file 22 | go.work 23 | -------------------------------------------------------------------------------- /.github/workflows/build_and_lint.yml: -------------------------------------------------------------------------------- 1 | name: Build and Lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v2 21 | with: 22 | go-version: '1.22' 23 | 24 | - name: Build 25 | run: go build 26 | 27 | - name: Run appstreamlint 28 | run: | 29 | ./appstreamlint org.gnome.gnome-power-statistics.metainfo.xml 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # appstreamlint 2 | 3 | A minimalistic lint tool for AppStream MetaInfo files for applications that checks that the essential fields of the AppStream MetaInfo 1.0 specification are there. 4 | 5 | It roughly follows https://www.freedesktop.org/software/appstream/docs/chap-Quickstart.html#sect-Quickstart-DesktopApps. 6 | 7 | This is meant to satisfy the requirements for AppStream MetaInfo files shipped inside AppImages, in the hope that future AppStream MetaInfo specification won't contradict the AppStream MetaInfo 1.0 specification and hence the 1.0 version of the specification can stay the requirement for AppStream MetaInfo files shipped inside AppImages indefinitely. 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 probonopd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /org.gnome.gnome-power-statistics.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | org.gnome.gnome-power-statistics 5 | FSFAP 6 | GPL-2.0+ 7 | Power Statistics 8 | Observe power management 9 | 10 | 11 |

12 | Power Statistics is a program used to view historical and current battery 13 | information and will show programs running on your computer using power. 14 |

15 |

Example list:

16 | 20 |

21 | You probably only need to install this application if you are having problems 22 | with your laptop battery, or are trying to work out what programs are using 23 | significant amounts of power. 24 |

25 |
26 | 27 | org.gnome.gnome-power-statistics.desktop 28 | 29 | 30 | 31 | The options dialog 32 | http://www.hughsie.com/en_US/main.png 33 | 34 | 35 | http://www.hughsie.com/en_US/preferences.png 36 | 37 | 38 | 39 | http://www.gnome.org/projects/en_US/gnome-power-manager 40 | GNOME 41 | 42 | 43 | gnome-power-statistics 44 | 45 | 46 | 47 | 48 | 49 |

Fixes issues X, Y and Z

50 |
51 |
52 |
53 |
-------------------------------------------------------------------------------- /appstreamlint.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | ) 9 | 10 | type Launchable struct { 11 | Type string `xml:"type,attr"` 12 | Contents string `xml:",chardata"` 13 | } 14 | 15 | type Component struct { 16 | XMLName xml.Name `xml:"component"` 17 | Type string `xml:"type,attr"` 18 | ID string `xml:"id"` 19 | Name string `xml:"name"` 20 | Summary string `xml:"summary"` 21 | MetadataLicense string `xml:"metadata_license"` 22 | ProjectLicense string `xml:"project_license"` 23 | Description string `xml:"description"` 24 | Launchable Launchable `xml:"launchable"` 25 | Screenshots []Screenshot `xml:"screenshots>screenshot"` 26 | } 27 | 28 | type Screenshot struct { 29 | Caption string `xml:"caption"` 30 | Image Image `xml:"image"` 31 | Environment string `xml:"environment,attr"` 32 | } 33 | 34 | type Image struct { 35 | Type string `xml:"type,attr"` 36 | Width int `xml:"width,attr"` 37 | Height int `xml:"height,attr"` 38 | Source string `xml:",chardata"` 39 | } 40 | 41 | func main() { 42 | if len(os.Args) < 2 { 43 | fmt.Println("Usage: appstreamlint ") 44 | os.Exit(1) 45 | } 46 | filePath := os.Args[1] 47 | xmlFile, err := os.Open(filePath) 48 | if err != nil { 49 | fmt.Println("Error opening file:", err) 50 | os.Exit(1) 51 | } 52 | defer xmlFile.Close() 53 | 54 | byteValue, err := ioutil.ReadAll(xmlFile) 55 | if err != nil { 56 | fmt.Println("Error reading file:", err) 57 | os.Exit(1) 58 | } 59 | 60 | var component Component 61 | if err := xml.Unmarshal(byteValue, &component); err != nil { 62 | fmt.Println("Error parsing XML:", err) 63 | os.Exit(1) 64 | } 65 | 66 | // Check filename 67 | // While desktop-application metadata is commonly stored in /usr/share/metainfo/%{id}.metainfo.xml (with a .metainfo.xml extension), 68 | // using a .appdata.xml extension is also permitted for this component type for legacy compatibility. 69 | // NOTE: This implementation will accept both .metainfo.xml and .appdata.xml extensions because the AppStream format should always be backwards compatible. 70 | if filePath != component.ID+".appdata.xml" && filePath != component.ID+".metainfo.xml" { 71 | fmt.Println("Error: Filename must be the same as the ID with .appdata.xml extension") 72 | // Print the correct filename 73 | fmt.Println("Correct filename:", component.ID+".metainfo.xml") 74 | // Print the actual filename 75 | fmt.Println("Actual filename:", filePath) 76 | os.Exit(1) 77 | } 78 | 79 | // Check required fields 80 | requiredFields := map[string]string{ 81 | "Type": component.Type, 82 | "ID": component.ID, 83 | "Name": component.Name, 84 | "Summary": component.Summary, 85 | "MetadataLicense": component.MetadataLicense, 86 | "ProjectLicense": component.ProjectLicense, 87 | "Description": component.Description, 88 | "LaunchableType": component.Launchable.Type, 89 | "Launchable": component.Launchable.Contents, 90 | } 91 | 92 | for field, value := range requiredFields { 93 | if value == "" { 94 | fmt.Printf("Error: %s must not be empty\n", field) 95 | os.Exit(1) 96 | } 97 | } 98 | 99 | // The desktop-application component type is the same as the desktop component type - 100 | // desktop is the older type identifier for desktop-applications and should not be used for new metainfo files, 101 | // unless compatibility with very old AppStream tools (pre 2016) is still wanted. 102 | // NOTE: Both types will be accepted in this implementation, because the AppStream format should always be backwards compatible. 103 | if component.Type != "desktop-application" && component.Type != "desktop" { 104 | fmt.Println("Error: Type must be 'desktop-application'") 105 | os.Exit(1) 106 | } 107 | 108 | // For desktop applications, the tag value must follow the reverse-DNS scheme 109 | // (e.g. org.gnome.gedit, org.kde.dolphin, etc.) and must not contain any spaces or special characters. 110 | // NOTE: Reverse-DNS is not enforced in this implementation as we think it complicates things, especially for new developers without a domain. 111 | // Furthermore, the tag value used to contain the name of the desktop file with the .desktop extension, unforunately, this has changed over time 112 | // but the tag value should not change once it has been set for a given application. 113 | 114 | // https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-metadata_license 115 | // NOTE: The AppStream specification might allow more licenses than the ones listed below in the future. So this implementation will only inform the user 116 | // if the metadata license is not allowed. The user can then check the latest AppStream specification for the allowed licenses. 117 | allowedLicenses := []string{ 118 | "FSFAP", 119 | "MIT", 120 | "0BSD", 121 | "CC0-1.0", 122 | "CC-BY-3.0", 123 | "CC-BY-4.0", 124 | "CC-BY-SA-3.0", 125 | "CC-BY-SA-4.0", 126 | "GFDL-1.1", 127 | "GFDL-1.2", 128 | "GFDL-1.3", 129 | "BSL-1.0", 130 | "FTL", 131 | "FSFUL", 132 | } 133 | 134 | allowed := false 135 | for _, license := range allowedLicenses { 136 | if component.MetadataLicense == license { 137 | allowed = true 138 | break 139 | } 140 | } 141 | if !allowed { 142 | fmt.Println("Warning: Metadata license is not allowed") 143 | fmt.Println("Allowed licenses:", allowedLicenses) 144 | fmt.Println("Actual license:", component.MetadataLicense) 145 | } 146 | 147 | // The human-readable name of the application. This is the name you want users to see prior to installing the application. 148 | // Check that it is at least 2 characters long. 149 | if len(component.Name) < 2 { 150 | fmt.Println("Error: Name must be at least 2 characters long") 151 | os.Exit(1) 152 | } 153 | 154 | // A short summary on what this application does, roughly equivalent to the Comment field of the accompanying .desktop file of the application. 155 | // Check that it is at least 10 characters long. 156 | if len(component.Summary) < 10 { 157 | fmt.Println("Error: Summary must be at least 10 characters long") 158 | os.Exit(1) 159 | } 160 | 161 | // The tag has a required type property indicating the system that is used to launch the component. The following types are allowed: 162 | // desktop-id: The component is launched using a desktop file. The desktop file is identified by the tag value. 163 | // NOTE: This implementation will only accept the desktop-id type. 164 | // myapplication.desktop 165 | // So check the type attribute and the value. 166 | if component.Launchable.Type != "desktop-id" { 167 | fmt.Println("Error: Launchable type must be 'desktop-id'") 168 | fmt.Println("Actual launchable type:", component.Launchable.Type) 169 | os.Exit(1) 170 | } 171 | 172 | // https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html#tag-screenshots 173 | 174 | if len(component.Screenshots) == 0 { 175 | fmt.Println("Warning: No screenshots tag found") 176 | } else { 177 | if len(component.Screenshots) == 0 { 178 | fmt.Println("Error: No screenshot tag found inside screenshots tag") 179 | os.Exit(1) 180 | } 181 | 182 | for _, screenshot := range component.Screenshots { 183 | if screenshot.Image.Type != "source" && screenshot.Image.Type != "video" && screenshot.Image.Type != "" { 184 | fmt.Println("Error: Image type must be 'source' or 'video'") 185 | os.Exit(1) 186 | } 187 | if screenshot.Image.Type == "source" { 188 | 189 | // The image source must be a valid URL, starting with http:// or https:// and following RFC 3986. 190 | // NOTE: For simplicity, this implementation will only check if the source starts with http:// or https:// and ends with a valid image extension. 191 | if len(screenshot.Image.Source) < 7 || (screenshot.Image.Source[:7] != "http://" && screenshot.Image.Source[:8] != "https://") { 192 | fmt.Println("Error: Image source must start with http:// or https://") 193 | os.Exit(1) 194 | } 195 | // Check if the source ends with a valid image extension 196 | validExtensions := []string{".png", ".jpg", ".jpeg"} // NOTE: It is debatable whether other image extensions should be allowed 197 | valid := false 198 | for _, ext := range validExtensions { 199 | if len(screenshot.Image.Source) > len(ext) && screenshot.Image.Source[len(screenshot.Image.Source)-len(ext):] == ext { 200 | valid = true 201 | break 202 | } 203 | } 204 | if !valid { 205 | fmt.Println("Error: Image source must end with a valid image extension") 206 | fmt.Println("Valid extensions:", validExtensions) 207 | fmt.Println("Actual extension:", screenshot.Image.Source[len(screenshot.Image.Source)-4:]) 208 | os.Exit(1) 209 | } 210 | } 211 | } 212 | 213 | } 214 | 215 | fmt.Println("Validation complete.") 216 | } 217 | --------------------------------------------------------------------------------