-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Cleanup for the basic usable version
- Loading branch information
0 parents
commit fb89820
Showing
51 changed files
with
3,765 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
|
||
switchhost | ||
*.json | ||
*.etag |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# Switch Host | ||
|
||
Yet another local switch game backup management tool. | ||
This is designed to be left running in the background as a small, low resource service | ||
|
||
## Features | ||
|
||
1. Scans multiple folders for source files | ||
1. Optionally organise based on user specified pattern | ||
1. Supports TitleDB or reading files for names | ||
1. Serves files over FTP and HTTP, and supports generating a `json` index | ||
|
||
## Goals (Todo) | ||
|
||
1. Serves a minimal web-ui for administration | ||
1. Can automate compression of files | ||
|
||
## Keys (optional) | ||
|
||
Having a prod.keys file will allow you to ensure the files you have a correctly classified. The app will look for the `prod.keys` file in `${HOME}/.switch/` | ||
|
||
Note: Only the header_key, and the key_area_key_application_XX keys are required. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
package cnmt | ||
|
||
import ( | ||
"encoding/binary" | ||
"errors" | ||
"fmt" | ||
|
||
partitionfs "github.com/ralim/switchhost/formats/partitionFS" | ||
) | ||
|
||
// CNMT -> https://switchbrew.org/wiki/CNMT | ||
// A.K.A PackagedContentMeta | ||
|
||
const ( | ||
ContentMetaType_SystemProgram = 1 | ||
ContentMetaType_SystemData = 2 | ||
ContentMetaType_SystemUpdate = 3 | ||
ContentMetaType_BootImagePackage = 4 | ||
ContentMetaType_BootImagePackageSafe = 5 | ||
ContentMetaType_Application = 0x80 | ||
ContentMetaType_Patch = 0x81 | ||
ContentMetaType_AddOnContent = 0x82 | ||
ContentMetaType_Delta = 0x83 | ||
) | ||
|
||
type ContentType int | ||
|
||
//MetaType is the main type of the conents, so base game, updates, dlc etc | ||
type MetaType int | ||
|
||
const ( | ||
Unknown MetaType = 0 | ||
BaseGame MetaType = 1 | ||
Update MetaType = 2 | ||
DLC MetaType = 3 | ||
) | ||
|
||
type Content struct { | ||
Text string | ||
Type string | ||
ID string | ||
Size string | ||
Hash string | ||
KeyGeneration string | ||
} | ||
|
||
type ContentMetaAttributes struct { | ||
TitleId uint64 | ||
Version uint32 | ||
Type MetaType | ||
Contents map[string]Content | ||
} | ||
|
||
type ContentMeta struct { | ||
Text string | ||
Type string | ||
ID string | ||
Version int | ||
RequiredDownloadSystemVersion string | ||
Content []struct { | ||
Text string | ||
Type string | ||
ID string | ||
Size string | ||
Hash string | ||
KeyGeneration string | ||
} | ||
Digest string | ||
KeyGenerationMin string | ||
RequiredSystemVersion string | ||
OriginalId string | ||
} | ||
|
||
func (t *MetaType) String() string { | ||
switch *t { | ||
case Unknown: | ||
return "Unknown" | ||
case BaseGame: | ||
return "Base" | ||
case Update: | ||
return "Update" | ||
case DLC: | ||
return "DLC" | ||
default: | ||
return "Unknown" | ||
} | ||
} | ||
|
||
func ParseBinary(pfs0 *partitionfs.PartionFS, data []byte) (*ContentMetaAttributes, error) { | ||
if pfs0 == nil || len(pfs0.FileEntryTable) != 1 { | ||
return nil, errors.New("invalid PartionFS") | ||
} | ||
cnmtFile := pfs0.FileEntryTable[0] | ||
cnmt := data[int64(cnmtFile.StartOffset):] | ||
titleId := binary.LittleEndian.Uint64(cnmt[0:0x8]) | ||
version := binary.LittleEndian.Uint32(cnmt[0x8:0xC]) | ||
tableOffset := binary.LittleEndian.Uint16(cnmt[0xE:0x10]) | ||
contentEntryCount := binary.LittleEndian.Uint16(cnmt[0x10:0x12]) | ||
contents := map[string]Content{} | ||
for i := uint16(0); i < contentEntryCount; i++ { | ||
position := 0x20 /*size of cnmt header*/ + tableOffset + (i * uint16(0x38)) | ||
ncaId := cnmt[position+0x20 : position+0x20+0x10] | ||
contentType := "" | ||
switch cnmt[position+0x36] { | ||
case 0: | ||
contentType = "Meta" | ||
case 1: | ||
contentType = "Program" | ||
case 2: | ||
contentType = "Data" | ||
case 3: | ||
contentType = "Control" | ||
case 4: | ||
contentType = "HtmlDocument" | ||
case 5: | ||
contentType = "LegalInformation" | ||
case 6: | ||
contentType = "DeltaFragment" | ||
} | ||
contents[contentType] = Content{ID: fmt.Sprintf("%x", ncaId)} | ||
} | ||
metaType := Unknown | ||
switch cnmt[0xC] { | ||
case ContentMetaType_Application: | ||
metaType = BaseGame | ||
case ContentMetaType_AddOnContent: | ||
metaType = DLC | ||
case ContentMetaType_Patch: | ||
metaType = Update | ||
} | ||
|
||
return &ContentMetaAttributes{Contents: contents, Version: version, TitleId: titleId, Type: metaType}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
package istorage | ||
|
||
import ( | ||
"encoding/binary" | ||
"errors" | ||
"fmt" | ||
) | ||
|
||
type Header struct { | ||
HeaderSize uint64 | ||
DirHashTableOffset uint64 | ||
DirHashTableSize uint64 | ||
DirMetaTableOffset uint64 | ||
DirMetaTableSize uint64 | ||
FileHashTableOffset uint64 | ||
FileHashTableSize uint64 | ||
FileMetaTableOffset uint64 | ||
FileMetaTableSize uint64 | ||
DataOffset uint64 | ||
} | ||
|
||
type FileEntry struct { | ||
Parent uint32 | ||
Sibling uint32 | ||
Offset uint64 | ||
Size uint64 | ||
Hash uint32 | ||
Name string | ||
Name_size uint32 | ||
} | ||
|
||
const ( | ||
FileTableEntrySize = 0x20 | ||
) | ||
|
||
// ReadHeader will parse the header data section into the header struct | ||
func ReadHeader(data []byte) (*Header, error) { | ||
if len(data) < 8*10 { | ||
return nil, fmt.Errorf("IStorage length too short %d", len(data)) | ||
} | ||
//Read out the array of uint64 values | ||
values := make([]uint64, 10) | ||
for i := 0; i < 10; i++ { | ||
values[i] = binary.LittleEndian.Uint64(data[(8 * i):(8 * (i + 1))]) | ||
} | ||
header := &Header{ | ||
HeaderSize: values[0], | ||
DirHashTableOffset: values[1], | ||
DirHashTableSize: values[2], | ||
DirMetaTableOffset: values[3], | ||
DirMetaTableSize: values[4], | ||
FileHashTableOffset: values[5], | ||
FileHashTableSize: values[6], | ||
FileMetaTableOffset: values[7], | ||
FileMetaTableSize: values[8], | ||
DataOffset: values[9], | ||
} | ||
|
||
return header, nil | ||
} | ||
|
||
//ReadFileEntries Will return all of the Fileentry records contained in the data | ||
|
||
func ReadFileEntries(data []byte, header Header) (map[string]FileEntry, error) { | ||
if header.FileMetaTableOffset+header.FileMetaTableSize > uint64(len(data)) { | ||
return nil, errors.New("data too small / bad header") | ||
} | ||
dirBytes := data[header.FileMetaTableOffset : header.FileMetaTableOffset+header.FileMetaTableSize] | ||
result := map[string]FileEntry{} | ||
|
||
offset := uint32(0x0) | ||
for offset < uint32(header.FileHashTableSize) { | ||
entry := FileEntry{} | ||
entry.Parent = binary.LittleEndian.Uint32(dirBytes[offset : offset+0x4]) | ||
entry.Sibling = binary.LittleEndian.Uint32(dirBytes[offset+0x4 : offset+0x8]) | ||
entry.Offset = binary.LittleEndian.Uint64(dirBytes[offset+0x8 : offset+0x10]) | ||
entry.Size = binary.LittleEndian.Uint64(dirBytes[offset+0x10 : offset+0x18]) | ||
entry.Hash = binary.LittleEndian.Uint32(dirBytes[offset+0x18 : offset+0x1C]) | ||
entry.Name_size = binary.LittleEndian.Uint32(dirBytes[offset+0x1C : offset+0x20]) | ||
entry.Name = string(dirBytes[offset+FileTableEntrySize : (offset+FileTableEntrySize)+entry.Name_size]) | ||
result[entry.Name] = entry | ||
offset = offset + FileTableEntrySize + entry.Name_size | ||
} | ||
return result, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
package nacp | ||
|
||
import ( | ||
"encoding/binary" | ||
"errors" | ||
"fmt" | ||
"io" | ||
|
||
cnmt "github.com/ralim/switchhost/formats/CNMT" | ||
istorage "github.com/ralim/switchhost/formats/IStorage" | ||
nca "github.com/ralim/switchhost/formats/NCA" | ||
partitionfs "github.com/ralim/switchhost/formats/partitionFS" | ||
"github.com/ralim/switchhost/formats/utils" | ||
"github.com/ralim/switchhost/keystore" | ||
"github.com/ralim/switchhost/settings" | ||
) | ||
|
||
//https://switchbrew.org/wiki/NACP_Format | ||
type Language int | ||
|
||
const ( | ||
AmericanEnglish Language = 0 | ||
BritishEnglish Language = 1 | ||
Japanese Language = 2 | ||
French Language = 3 | ||
German Language = 4 | ||
LatinAmericanSpanish Language = 5 | ||
Spanish Language = 6 | ||
Italian Language = 7 | ||
Dutch Language = 8 | ||
CanadianFrench Language = 9 | ||
Portuguese Language = 10 | ||
Russian Language = 11 | ||
Korean Language = 12 | ||
TraditionalChinese Language = 13 | ||
SimplifiedChinese Language = 14 | ||
) | ||
|
||
type NacpTitleEntry struct { | ||
Language Language | ||
Title string | ||
} | ||
|
||
type NACP struct { | ||
Titles map[Language]NacpTitleEntry | ||
DisplayVersion string | ||
SupportedLanguageFlags uint32 | ||
} | ||
|
||
func ExtractNACP(keystore *keystore.Keystore, cnmt *cnmt.ContentMetaAttributes, file io.ReaderAt, securePartition *partitionfs.PartionFS, securePartitionOffset uint64) (*NACP, error) { | ||
if control, ok := cnmt.Contents["Control"]; ok { | ||
controlNca := securePartition.GetByName(control.ID) | ||
if controlNca == nil { | ||
return nil, fmt.Errorf("unable to find control.nacp by id %v", control.ID) | ||
} | ||
|
||
NCAMetaHeader, err := nca.ParseNCAEncryptedHeader(keystore, file, securePartitionOffset+controlNca.StartOffset) | ||
if err != nil { | ||
return nil, fmt.Errorf("parsing NCA encrypted header failed with - %w", err) | ||
} | ||
fsHeader, err := nca.GetFSHeader(NCAMetaHeader, 0) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
section, err := nca.DecryptMetaNCADataSection(keystore, file, NCAMetaHeader, securePartitionOffset+controlNca.StartOffset) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if fsHeader.FSType == 0 { | ||
romFsHeader, err := istorage.ReadHeader(section) | ||
if err != nil { | ||
return nil, err | ||
} | ||
fEntries, err := istorage.ReadFileEntries(section, *romFsHeader) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if entry, ok := fEntries["control.nacp"]; ok { | ||
nacp, err := ReadNACP(section, *romFsHeader, entry) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return &nacp, nil | ||
} | ||
} else { | ||
return nil, errors.New("unsupported type " + control.ID) | ||
} | ||
|
||
} | ||
return nil, errors.New("no control.nacp found") | ||
} | ||
|
||
func ReadNACP(data []byte, romFsHeader istorage.Header, fileEntry istorage.FileEntry) (NACP, error) { | ||
offset := romFsHeader.DataOffset + fileEntry.Offset | ||
titles := map[Language]NacpTitleEntry{} | ||
for i := 0; i < 16; i++ { | ||
appTitleBytes := data[offset+(uint64(i)*0x300) : offset+(uint64(i)*0x300)+0x200] | ||
nameBytes := utils.CString(appTitleBytes) | ||
titles[Language(i)] = NacpTitleEntry{Language: Language(i), Title: string(nameBytes)} | ||
} | ||
|
||
displayVersion := utils.CString(data[offset+0x3060 : offset+0x3060+0x10]) | ||
supportedLanguageFlags := binary.BigEndian.Uint32(data[offset+0x302C : offset+0x302C+0x4]) | ||
|
||
return NACP{Titles: titles, DisplayVersion: string(displayVersion), SupportedLanguageFlags: supportedLanguageFlags}, nil | ||
|
||
} | ||
|
||
func (n *NACP) GetSuggestedTitle(settings *settings.Settings) string { | ||
// Return the titles in preferred order | ||
// If not fall back by Language order | ||
for index := range settings.PreferredLangOrder { | ||
v, ok := n.Titles[Language(index)] | ||
if ok { | ||
if len(v.Title) > 0 { | ||
return v.Title | ||
} | ||
} | ||
} | ||
|
||
for _, v := range n.Titles { | ||
if len(v.Title) > 0 { | ||
return v.Title | ||
} | ||
} | ||
return "" | ||
} |
Oops, something went wrong.