Skip to content

Commit

Permalink
Inital version
Browse files Browse the repository at this point in the history
Cleanup for the basic usable version
  • Loading branch information
Ralim committed Dec 16, 2021
0 parents commit fb89820
Show file tree
Hide file tree
Showing 51 changed files with 3,765 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

switchhost
*.json
*.etag
22 changes: 22 additions & 0 deletions README.md
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.
133 changes: 133 additions & 0 deletions formats/CNMT/cnmt.go
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
}
85 changes: 85 additions & 0 deletions formats/IStorage/istorage.go
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
}
129 changes: 129 additions & 0 deletions formats/NACP/nacp.go
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 ""
}
Loading

0 comments on commit fb89820

Please sign in to comment.