Now repository can have apps and allowed to install them

This commit is contained in:
doesnm 2025-05-20 19:05:15 +00:00
parent d004e69e6b
commit 71c1312df9
4 changed files with 512 additions and 7 deletions

28
apktest.go Normal file
View file

@ -0,0 +1,28 @@
package main
import (
"encoding/xml"
"fmt"
"github.com/avast/apkparser"
"os"
)
func main() {
enc := xml.NewEncoder(os.Stdout)
//enc.Indent("", "\t")
zipErr, resErr, manErr := apkparser.ParseApk(os.Args[1], enc)
if zipErr != nil {
fmt.Fprintf(os.Stderr, "Failed to open the APK: %s", zipErr.Error())
os.Exit(1)
return
}
if resErr != nil {
fmt.Fprintf(os.Stderr, "Failed to parse resources: %s", resErr.Error())
}
if manErr != nil {
fmt.Fprintf(os.Stderr, "Failed to parse AndroidManifest.xml: %s", manErr.Error())
os.Exit(1)
return
}
}

5
go.mod
View file

@ -3,3 +3,8 @@ module fdroidgo
go 1.24.2
require github.com/BurntSushi/toml v1.5.0
require (
github.com/avast/apkparser v0.0.0-20250423072857-abc1843ceb56 // indirect
github.com/klauspost/compress v1.18.0 // indirect
)

4
go.sum
View file

@ -1,2 +1,6 @@
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/avast/apkparser v0.0.0-20250423072857-abc1843ceb56 h1:gFlKwiEFHiSUH79HIhSa2jJmM6gZg4xQaZIkMjUS73c=
github.com/avast/apkparser v0.0.0-20250423072857-abc1843ceb56/go.mod h1:3F9A8btIerUcuy7Fmno+g/nIk4ELKJ6NCs2/KK1bvLs=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=

482
main.go
View file

@ -8,9 +8,20 @@ import (
"io/ioutil"
"os"
"os/exec"
"github.com/BurntSushi/toml"
"time"
"log"
"github.com/BurntSushi/toml"
"time"
"log"
"net/http"
"strings"
"bytes"
"encoding/xml"
"github.com/avast/apkparser"
"strconv"
"crypto/sha256"
"encoding/hex"
"path/filepath"
"net/url"
"path"
)
type Repo struct {
@ -55,6 +66,7 @@ type Index struct {
Repo Repo `json:"repo"`
Requests Requests `json:"requests"`
Apps []App `json:"apps"`
Packages map[string][]Package `json:"packages"`
}
type Config struct {
@ -62,6 +74,389 @@ type Config struct {
Icon string `toml:"icon"`
Address string `toml:"address"`
Description string `toml:"description"`
KeyStorePath string `toml:"keystore_path"`
KeyStorePassword string `toml:"keystore_password"`
KeyStoreAlias string `toml:"keystore_alias"`
BinarySources []string `toml:"binarySources"`
}
type User struct {
Name *string `json:"name,omitempty"`
Login string `json:"login"`
}
type ReleaseAsset struct {
BrowserDownloadURL string `json:"browser_download_url"`
ID int `json:"id"`
Size int `json:"size"`
CreatedAt time.Time `json:"created_at"`
Uploader *User `json:"uploader,omitempty"`
}
type Release struct {
TagName string `json:"tag_name"`
Name *string `json:"name,omitempty"`
CreatedAt time.Time `json:"created_at"`
PublishedAt *time.Time `json:"published_at,omitempty"`
Author User `json:"author"`
Assets []ReleaseAsset `json:"assets"`
}
type Repository struct {
License *License `json:"license"`
HTMLURL string `json:"html_url"`
IssuesUrl string `json:"issues_url"`
Homepage string `json:"homepage"`
Description string `json:"description"`
}
type License struct {
Name string `json:"name"`
SPDXID string `json:"spdx_id"`
}
type Package struct {
Added int64 `json:"added"`
ApkName string `json:"apkName"`
Hash string `json:"hash"`
HashType string `json:"hashType"`
MinSdkVersion int `json:"minSdkVersion"`
NativeCode []string `json:"nativecode"`
PackageName string `json:"packageName"`
Sig string `json:"sig"`
Signer string `json:"signer"`
Size int64 `json:"size"`
TargetSdkVersion int `json:"targetSdkVersion"`
UsesPermission [][]*string `json:"uses-permission"`
UsesPermissionSdk23 [][]string `json:"uses-permission-sdk-23"`
VersionCode int64 `json:"versionCode"`
VersionName string `json:"versionName"`
ApplicationName string `json:"-"`
}
type Application struct {
Label string `xml:"label,attr"`
Icon string `xml:"icon,attr"`
// Add other application attributes as needed
}
type Manifest struct {
PackageName string `xml:"package,attr"`
VersionCode int64 `xml:"versionCode,attr"`
VersionName string `xml:"versionName,attr"`
UsesSdk UsesSdk `xml:"uses-sdk"`
UsesPermission []UsesPermission `xml:"uses-permission"`
Application Application `xml:"application"`
}
// Structure for <uses-sdk>
type UsesSdk struct {
MinSdkVersion string `xml:"minSdkVersion,attr"`
TargetSdkVersion string `xml:"targetSdkVersion,attr"`
}
// Structure for <uses-permission>
type UsesPermission struct {
Name string `xml:"name,attr"`
}
// Structure for <uses-feature>
func apkExists(filePath string) bool {
_, err := os.Stat(filePath)
return !os.IsNotExist(err)
}
// Function to check if the URL is an APK
func isAPK(url string) bool {
return strings.HasSuffix(url, ".apk")
}
// Function to download the APK file
func downloadAPK(url string, filePath string) error {
fmt.Printf("Downloading %s",url)
response, err := http.Get(url)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return fmt.Errorf("failed to download file: %s", response.Status)
}
file, err := os.Create(filePath)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(file, response.Body)
return err
}
func getResourceTypeId(resourceType string) int {
switch resourceType {
case "string":
return 0
case "drawable":
return 1
case "layout":
return 2
case "mipmap":
return 3
case "color":
return 4
case "id":
return 5
case "style":
return 6
case "anim":
return 7
case "menu":
return 8
case "xml":
return 9
case "raw":
return 10
case "font":
return 11
case "transition":
return 12
case "drawable-v21":
return 13
default:
return -1 // Invalid resource type
}
}
func saveIconFile(f *apkparser.ZipReaderFile, packageName string) string {
// Create the directory if it does not exist
err := os.MkdirAll(packageName + "/en-US", os.ModePerm)
if err != nil {
fmt.Println("Error creating directory:", err)
return ""
}
// Define the path for the icon file
iconPath := filepath.Join(packageName, "en-US")
iconPath = filepath.Join(iconPath,"icon.png")
// Create a new file to save the icon
outFile, err := os.Create(iconPath)
if err != nil {
fmt.Println("Error creating icon file:", err)
return ""
}
defer outFile.Close()
// Open the zip file for reading
err = f.Open()
if err != nil {
fmt.Println("Error opening zip file:", err)
return ""
}
defer f.Close()
// Copy the contents of the zip file to the new file
if _, err := io.Copy(outFile, f); err != nil {
fmt.Println("Error copying icon file:", err)
return ""
}
return iconPath
}
func processAPK(filePath string) Package {
fmt.Printf("Processing APK: %s\n", filePath)
var buf bytes.Buffer
enc := xml.NewEncoder(&buf)
zipFile, err := os.Open(filePath)
if err != nil {
fmt.Println("Error opening ZIP file:", err)
os.Exit(1)
return Package{}
}
defer zipFile.Close()
// Get the file info
fileInfo, err := zipFile.Stat()
if err != nil {
fmt.Println("Error getting file info:", err)
os.Exit(1)
return Package{}
}
// Create a zip.Reader
zipReader, zipErr := apkparser.OpenZipReader(zipFile)
p,resErr := apkparser.NewParser(zipReader,enc)
manErr := p.ParseXml("AndroidManifest.xml")
if zipErr != nil {
fmt.Fprintf(os.Stderr, "Failed to open the APK: %s\n", zipErr.Error())
os.Exit(1)
}
if resErr != nil {
fmt.Fprintf(os.Stderr, "Failed to parse resources: %s\n", resErr.Error())
}
if manErr != nil {
fmt.Fprintf(os.Stderr, "Failed to parse AndroidManifest.xml: %s\n", manErr.Error())
os.Exit(1)
}
manifestXML := buf.String()
var manifest Manifest
if err := xml.Unmarshal([]byte(manifestXML), &manifest); err != nil {
fmt.Fprintf(os.Stderr, "Failed to unmarshal manifest XML: %s\n", err.Error())
os.Exit(1)
}
minsdk, err := strconv.Atoi(manifest.UsesSdk.MinSdkVersion)
if err != nil {
log.Fatal("Can't parse minsdk: ", manifest.UsesSdk.MinSdkVersion)
os.Exit(1)
}
targetsdk, err := strconv.Atoi(manifest.UsesSdk.TargetSdkVersion)
if err != nil {
log.Fatal("Can't parse targetsdk: ", manifest.UsesSdk.TargetSdkVersion)
os.Exit(1)
}
// Calculate SHA-256 hash of the APK file
hash, err := calculateSHA256(filePath)
if err != nil {
log.Fatalf("Failed to calculate SHA-256 hash: %s\n", err.Error())
os.Exit(1)
}
packageInfo := Package{
ApkName: filePath,
VersionCode: manifest.VersionCode,
VersionName: manifest.VersionName,
MinSdkVersion: minsdk,
TargetSdkVersion: targetsdk,
PackageName: manifest.PackageName,
UsesPermission: make([][]*string, len(manifest.UsesPermission)),
ApplicationName: manifest.Application.Label,
Hash: hash,
HashType: "SHA-256",
Size: fileInfo.Size(),
}
for i, perm := range manifest.UsesPermission {
packageInfo.UsesPermission[i] = append(packageInfo.UsesPermission[i], &perm.Name)
packageInfo.UsesPermission[i] = append(packageInfo.UsesPermission[i], nil)
}
if !strings.HasPrefix(manifest.Application.Icon, "@") {
iconFile := zipReader.File[manifest.Application.Icon]
saveIconFile(iconFile,manifest.PackageName)
}
return packageInfo
}
// calculateSHA256 computes the SHA-256 hash of the given file.
func calculateSHA256(filePath string) (string, error) {
fmt.Println("Calculating SHA256")
file, err := os.Open(filePath)
if err != nil {
return "", err
}
defer file.Close()
hasher := sha256.New()
if _, err := io.Copy(hasher, file); err != nil {
return "", err
}
hash := hasher.Sum(nil)
return hex.EncodeToString(hash), nil
}
func getFilenameFromURL(urlStr string) (string, error) {
// Parse the URL
parsedURL, err := url.Parse(urlStr)
if err != nil {
return "", err
}
// Get the path from the URL and extract the filename
filename := path.Base(parsedURL.Path)
return filename, nil
}
// Function to handle downloading and processing APKs from a release
func handleRelease(release Release) []Package {
var packages []Package
for _, asset := range release.Assets {
if !isAPK(asset.BrowserDownloadURL) {
//fmt.Printf("Skipping non-APK file: %s\n", asset.BrowserDownloadURL)
continue
}
filePath,err := getFilenameFromURL(asset.BrowserDownloadURL)
if apkExists(asset.BrowserDownloadURL) {
fmt.Printf("APK already exists: %s\n", asset.BrowserDownloadURL)
continue
}
err = downloadAPK(asset.BrowserDownloadURL, filePath)
if err != nil {
fmt.Printf("Error downloading APK: %v\n", err)
os.Exit(1)
continue
}
packageInfo := processAPK(filePath)
packages = append(packages,packageInfo)
}
return packages
}
func getReleases(repo string) ([]Release, error) {
url := fmt.Sprintf("https://api.github.com/repos/%s/releases", repo)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to get releases: %s", resp.Status)
os.Exit(1)
}
var releases []Release
// Декодируем JSON-ответ в структуру Release
// Декодируем JSON-ответ в pструктуру Release
if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil {
return nil, err
}
return releases, nil
}
func getRepositoryInfo(repo string) (Repository, error) {
fmt.Println("Fetching repository info")
url := fmt.Sprintf("https://api.github.com/repos/%s", repo)
resp, err := http.Get(url)
if err != nil {
return Repository{}, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return Repository{}, fmt.Errorf("failed to get repository info: %s", resp.Status)
}
var repository Repository
if err := json.NewDecoder(resp.Body).Decode(&repository); err != nil {
return Repository{}, err
}
return repository, nil
}
func generateJSON(index Index, filename string) error {
@ -114,7 +509,48 @@ func signJarWithJarsigner(jarFile string, keystore string, password string, alia
return err, string(output) // Возвращаем ошибку и вывод как строку
}
func createAppFromReleases(releases []Release, repo string) (App, []Package,error) {
// Получаем информацию о репозитории
repository, err := getRepositoryInfo(repo)
if err != nil {
return App{}, []Package{}, err
}
// Предположим, что мы берем данные из первого релиза
if len(releases) == 0 {
return App{}, []Package{}, nil
}
release := releases[0]
packages := handleRelease(release)
app := App{
AuthorName: release.Author.Login,
Categories: []string{"fdroid"},
SuggestedVersionCode: release.TagName,
IssueTracker: repository.HTMLURL + "/issues",
License: repository.License.SPDXID,
Name: packages[0].ApplicationName,
SourceCode: repository.HTMLURL,
Summary: repository.Description,
WebSite: repository.Homepage,
Added: time.Now().Unix(),
PackageName: packages[0].PackageName,
LastUpdated: release.CreatedAt.Unix(),
Localized: Localized{
ENUS: Icon {
Icon: "icon.png",
},
},
}
return app, packages, nil
}
func main() {
var apps []App
var packages map[string][]Package
packages = make(map[string][]Package)
var config Config
file, err := os.Open("config.toml")
if err != nil {
@ -126,6 +562,37 @@ func main() {
if _, err := toml.NewDecoder(file).Decode(&config); err != nil {
log.Fatal(err)
}
for _, source := range config.BinarySources {
fmt.Printf("Processing %s\n",source)
// Извлекаем имя репозитория из ссылки
repo := strings.TrimPrefix(source, "github.com/")
if repo == source { // Если не удалось извлечь, пропускаем
fmt.Printf("Invalid repository format: %s\n", source)
continue
}
// Получаем релизы для репозитория
fmt.Println("Fetching releases")
releases, err := getReleases(repo)
if err != nil {
fmt.Printf("Error fetching releases for %s: %v\n", repo, err)
os.Exit(1)
continue
}
var app App
fmt.Println("Creating app object from repository info and apk")
app,app_packages, err := createAppFromReleases(releases,repo)
if err != nil {
log.Fatal("Failed to create app from releases: ",err)
}
apps = append(apps,app)
packages[app_packages[0].PackageName] = append(packages[app_packages[0].PackageName], app_packages...)
}
index := Index {
Repo: Repo{
Timestamp: time.Now().Unix(),
@ -139,7 +606,8 @@ func main() {
Install: []string{},
Uninstall: []string{},
},
Apps: []App{},
Apps: apps,
Packages: packages,
}
// Генерация JSON файла
err = generateJSON(index, "index-v1.json")
@ -158,9 +626,9 @@ func main() {
fmt.Println("JAR file created successfully.")
// Подпись JAR файла
p12File := "../../keystore.p12" // Укажите путь к вашему P12 файлу
password := "u86viXB0FRlTY2K1buzGqWHdDu6pIgu8520R3IJgXAE=" // Укажите пароль для P12 файла
alias := "curious-invention.aeza.network" // Укажите алиас для ключа
p12File := config.KeyStorePath // Укажите путь к вашему P12 файлу
password := config.KeyStorePassword // Укажите пароль для P12 файла
alias := config.KeyStoreAlias // Укажите алиас для ключа
err, output := signJarWithJarsigner("index-v1.jar", p12File, password, alias)
if err != nil {