├── docs ├── articles │ ├── specs │ │ ├── cartridge │ │ │ └── ncsd.md │ │ ├── tutorial.md │ │ └── toc.yml │ ├── dev │ │ ├── tutorial.md │ │ ├── features │ │ │ └── cartridge.md │ │ └── toc.yml │ └── changelog.md ├── api │ └── .gitignore ├── images │ ├── favicon.png │ ├── logo-128.png │ └── logo-48.png ├── .gitignore ├── template │ └── public │ │ ├── main.js │ │ └── main.css ├── toc.yml ├── docfx.json └── index.md ├── .prettierrc.yaml ├── src ├── Lemon.IntegrationTests │ ├── Resources │ │ ├── containers │ │ │ ├── .gitattributes │ │ │ ├── .gitignore │ │ │ ├── aofm_manual.yml │ │ │ ├── pwaat_manual.yml │ │ │ ├── pwdd_manual.yml │ │ │ ├── fake_ncch.yml │ │ │ ├── fake.ncsd │ │ │ ├── exefs.txt │ │ │ ├── ivfc.txt │ │ │ ├── ncsd.txt │ │ │ ├── cia.txt │ │ │ ├── pwdd_program.yml │ │ │ ├── aofm_program.yml │ │ │ ├── update_program.yml │ │ │ ├── pwaat_program.yml │ │ │ ├── update.yml │ │ │ ├── fake_ncsd.yml │ │ │ ├── tloz_oot3d.yml │ │ │ ├── nand.yml │ │ │ ├── ncch.txt │ │ │ ├── fake_exefs.yml │ │ │ ├── aofm_exefs.yml │ │ │ ├── aofm_cia.yml │ │ │ ├── aofm_cia_legit.yml │ │ │ └── aofm_ivfc.yml │ │ └── titles │ │ │ ├── tmd.txt │ │ │ ├── aofm_tmd.yml │ │ │ └── aofm_tmd_legit.yml │ ├── Lemon.IntegrationTests.csproj │ ├── Extensions │ │ └── FluentAssertionsExtensions.cs │ ├── Containers │ │ ├── NcchTestInfo.cs │ │ ├── NcsdTestInfo.cs │ │ ├── NodeContainerInfo.cs │ │ ├── CiaConverterTests.cs │ │ ├── IvfcConverterTests.cs │ │ ├── ExeFsConverterTests.cs │ │ ├── TestData.cs │ │ ├── NcsdConverterTests.cs │ │ ├── Binary2ContainerTests.cs │ │ └── NcchConverterTests.cs │ ├── Titles │ │ ├── TestData.cs │ │ ├── Binary2TitleMetadataTests.cs │ │ └── Binary2ObjectTests.cs │ └── TestDataBase.cs ├── nuget.config ├── Lemon │ ├── LoggerFactory.cs │ ├── Lemon.csproj │ ├── Containers │ │ ├── Formats │ │ │ ├── FirmwareType.cs │ │ │ ├── Ncch.cs │ │ │ ├── Ncsd.cs │ │ │ ├── NcchHeader.cs │ │ │ └── NcsdHeader.cs │ │ ├── Converters │ │ │ ├── Ivfc │ │ │ │ ├── BlockWrittenEventArgs.cs │ │ │ │ ├── NameHash.cs │ │ │ │ └── LevelStream.cs │ │ │ ├── Binary2Ncsd.cs │ │ │ ├── Binary2Ncch.cs │ │ │ ├── BinaryCia2NodeContainer.cs │ │ │ ├── BinaryExeFs2NodeContainer.cs │ │ │ ├── BinaryIvfc2NodeContainer.cs │ │ │ ├── NodeContainer2BinaryIvfc.cs │ │ │ ├── NodeContainer2BinaryCia.cs │ │ │ └── Ncch2Binary.cs │ │ └── ContainerManager.cs │ └── Titles │ │ ├── ContentAttributes.cs │ │ ├── ContentInfoRecord.cs │ │ ├── ContentChunkRecord.cs │ │ ├── TitleMetadata.cs │ │ ├── Binary2TitleMetadata.cs │ │ └── TitleMetadata2Binary.cs ├── Tests.runsettings ├── Directory.Packages.props ├── Directory.Build.props └── Lemon.sln ├── GitVersion.yml ├── CONTRIBUTING.md ├── .gitignore ├── .vscode ├── settings.json ├── launch.json └── tasks.json ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build-and-release.yml │ ├── build.yml │ └── deploy.yml ├── SECURITY.md ├── .config └── dotnet-tools.json ├── LICENSE ├── GitReleaseManager.yaml ├── CODE_OF_CONDUCT.md └── README.md /docs/articles/specs/cartridge/ncsd.md: -------------------------------------------------------------------------------- 1 | # NCSD 2 | 3 | TODO 4 | -------------------------------------------------------------------------------- /docs/articles/dev/tutorial.md: -------------------------------------------------------------------------------- 1 | # Getting started guide 2 | 3 | TODO 4 | -------------------------------------------------------------------------------- /docs/articles/specs/tutorial.md: -------------------------------------------------------------------------------- 1 | # Getting started guide 2 | 3 | TODO... 4 | -------------------------------------------------------------------------------- /docs/articles/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | To be filled on preview builds. 4 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | overrides: 2 | - files: "*.md" 3 | options: 4 | proseWrap: always 5 | -------------------------------------------------------------------------------- /docs/articles/dev/features/cartridge.md: -------------------------------------------------------------------------------- 1 | # Cartridge / ROM converter and format 2 | 3 | TODO 4 | -------------------------------------------------------------------------------- /docs/api/.gitignore: -------------------------------------------------------------------------------- 1 | ############### 2 | # temp file # 3 | ############### 4 | *.yml 5 | .manifest 6 | -------------------------------------------------------------------------------- /docs/articles/specs/toc.yml: -------------------------------------------------------------------------------- 1 | - name: 📁 Cartridge 2 | - name: 🚧 NCSD 3 | href: cartridge/ncsd.md 4 | -------------------------------------------------------------------------------- /docs/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SceneGate/Lemon/develop/docs/images/favicon.png -------------------------------------------------------------------------------- /docs/images/logo-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SceneGate/Lemon/develop/docs/images/logo-128.png -------------------------------------------------------------------------------- /docs/images/logo-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SceneGate/Lemon/develop/docs/images/logo-48.png -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/.gitattributes: -------------------------------------------------------------------------------- 1 | fake.ncsd filter=lfs diff=lfs merge=lfs -text 2 | -------------------------------------------------------------------------------- /GitVersion.yml: -------------------------------------------------------------------------------- 1 | mode: ContinuousDeployment 2 | branches: 3 | develop: 4 | tag: preview 5 | increment: Patch 6 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/.gitignore: -------------------------------------------------------------------------------- 1 | # Test resources 2 | *.3ds 3 | *.csu 4 | nand.bin 5 | *.cia 6 | 7 | !fake* 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Lemon 2 | 3 | Refer to the 4 | [Yarhl contributing guidelines](https://github.com/SceneGate/Yarhl/blob/master/CONTRIBUTING.md). 5 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/aofm_manual.yml: -------------------------------------------------------------------------------- 1 | signature_length: 256 2 | regions_offset: [4096] 3 | regions_size: [405504] 4 | available_regions: ["rom"] 5 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/pwaat_manual.yml: -------------------------------------------------------------------------------- 1 | signature_length: 256 2 | regions_offset: [4096] 3 | regions_size: [880640] 4 | available_regions: ["rom"] 5 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/pwdd_manual.yml: -------------------------------------------------------------------------------- 1 | signature_length: 256 2 | regions_offset: [4096] 3 | regions_size: [1961984] 4 | available_regions: ["rom"] 5 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/fake_ncch.yml: -------------------------------------------------------------------------------- 1 | signature_length: 256 2 | regions_offset: [512, 2560] 3 | regions_size: [2048, 512] 4 | available_regions: ["system", "rom"] 5 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/titles/tmd.txt: -------------------------------------------------------------------------------- 1 | # Optionals not committed 2 | aofm_tmd.yml,../containers/aofm.cia,11712,2916 3 | aofm_tmd_legit.yml,../containers/aofm_legit.cia,11712,2916 4 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/fake.ncsd: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:37c4a6f598937a7fce5313ba148359f503e7b283323fc86c8c6c84ce8497c322 3 | size 20480 4 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | ############### 2 | # folder # 3 | ############### 4 | /**/DROP/ 5 | /**/TEMP/ 6 | /**/packages/ 7 | /**/bin/ 8 | /**/obj/ 9 | _site 10 | 11 | # DrawIO 12 | .$*.drawio* 13 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/exefs.txt: -------------------------------------------------------------------------------- 1 | # Manually created and committed 2 | fake_exefs.yml,fake.ncsd,16896,2048 3 | 4 | # Optionals not committed 5 | aofm_exefs.yml,aofm.cia,17728,1169408 6 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/ivfc.txt: -------------------------------------------------------------------------------- 1 | # Manually created and committed 2 | # fake_exefs.yml,fake.ncsd,17920,512 3 | 4 | # Optionals not committed 5 | aofm_ivfc.yml,aofm.cia,1190208,184934400 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build outputs 2 | obj/ 3 | bin/ 4 | build/artifacts/ 5 | build/temp/ 6 | /CHANGELOG.md 7 | /CHANGELOG.NEXT.md 8 | 9 | # IDEs 10 | .vs/ 11 | *.csproj.user 12 | *.DotSettings.user 13 | launchSettings.json 14 | -------------------------------------------------------------------------------- /docs/template/public/main.js: -------------------------------------------------------------------------------- 1 | export default { 2 | iconLinks: [ 3 | { 4 | icon: "github", 5 | href: "https://github.com/SceneGate/Lemon", 6 | title: "GitHub", 7 | }, 8 | ], 9 | }; 10 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/ncsd.txt: -------------------------------------------------------------------------------- 1 | # Manually created and committed 2 | fake.ncsd,fake_ncsd.yml 3 | 4 | # Optionals not committed 5 | tloz_oot3d.3ds,tloz_oot3d.yml 6 | update.csu,update.yml 7 | nand.bin,nand.yml 8 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/cia.txt: -------------------------------------------------------------------------------- 1 | # Optionals not committed 2 | # Due to a bug in GodMode9, CIA dumps have junk that must be 0 between 0x3924 and 0x3940 3 | aofm_cia.yml,aofm.cia 4 | aofm_cia_legit.yml,aofm_legit.cia 5 | -------------------------------------------------------------------------------- /docs/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Dev guides 2 | href: articles/dev/ 3 | 4 | - name: Format specs 5 | href: articles/specs/ 6 | 7 | - name: Changelog 8 | href: articles/changelog.md 9 | 10 | - name: GitHub 11 | href: https://github.com/SceneGate/Lemon 12 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/pwdd_program.yml: -------------------------------------------------------------------------------- 1 | signature_length: 256 2 | regions_offset: [512, 1536, 2560, 3072, 2998272] 3 | regions_size: [1024, 1024, 512, 2993152, 575582208] 4 | available_regions: ["extended_header", "access_descriptor", "sdk_info.txt", "system", "rom"] 5 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/aofm_program.yml: -------------------------------------------------------------------------------- 1 | signature_length: 256 2 | regions_offset: [0x200, 0x600, 2560, 3072, 1175552] 3 | regions_size: [0x400, 0x400, 512, 1169408, 184934400] 4 | available_regions: ["extended_header", "access_descriptor", "sdk_info.txt", "system", "rom"] 5 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/update_program.yml: -------------------------------------------------------------------------------- 1 | signature_length: 256 2 | regions_offset: [0x200, 0x600, 2560, 10752, 421888] 3 | regions_size: [0x400, 0x400, 8192, 410624, 147668992] 4 | available_regions: ["extended_header", "access_descriptor", "logo.bin", "system", "rom"] 5 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/pwaat_program.yml: -------------------------------------------------------------------------------- 1 | signature_length: 256 2 | regions_offset: [512, 1536, 10752, 2560, 11264, 1323008] 3 | regions_size: [1024, 1024, 512, 8192, 1311232, 382976000] 4 | available_regions: ["extended_header", "access_descriptor", "sdk_info.txt", "logo.bin", "system", "rom"] 5 | -------------------------------------------------------------------------------- /docs/articles/dev/toc.yml: -------------------------------------------------------------------------------- 1 | - name: ✨ Getting started 2 | - name: Introduction 3 | href: ../../index.md 4 | - name: 🚧 Getting started guide 5 | href: tutorial.md 6 | 7 | - name: ♻️ Converters 8 | - name: 🚧 Cartridge 9 | href: features/cartridge.md 10 | 11 | - name: 📃 API docs 12 | - name: Namespaces 13 | href: ../../api/toc.yml 14 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/update.yml: -------------------------------------------------------------------------------- 1 | signature_length: 256 2 | size: 2147483648 3 | media_id: 1125900174492161 4 | firmwares_type: ["None", "None", "None", "None", "None", "None", "None", "None"] 5 | crypt_type: [0, 0, 0, 0, 0, 0, 0, 0] 6 | partitions_offset: [16384] 7 | partitions_size: [148090880] 8 | available_partitions: ["program"] 9 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/fake_ncsd.yml: -------------------------------------------------------------------------------- 1 | signature_length: 256 2 | size: 18432 3 | media_id: 17279655951921914625 4 | firmwares_type: ["None", "None", "None", "None", "None", "None", "None", "None"] 5 | crypt_type: [0, 0, 0, 0, 0, 0, 0, 0] 6 | partitions_offset: [16384, 19456] 7 | partitions_size: [3072, 1024] 8 | available_partitions: ["program", "manual"] 9 | -------------------------------------------------------------------------------- /src/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/tloz_oot3d.yml: -------------------------------------------------------------------------------- 1 | signature_length: 256 2 | size: 0x20000000 # 512 MB 3 | media_id: 0x0004000000033600 4 | firmwares_type: ["None", "None", "None", "None", "None", "None", "None", "None"] 5 | crypt_type: [0, 0, 0, 0, 0, 0, 0, 0] 6 | partitions_offset: [0x4000, 0x1CC0B800, 0x1CD43800] 7 | partitions_size: [0x1CC07800, 0x138000, 0x21E6200] 8 | available_partitions: ["program", "manual", "update"] 9 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/nand.yml: -------------------------------------------------------------------------------- 1 | signature_length: 256 2 | size: 1342177280 3 | media_id: 0 4 | firmwares_type: ["Normal", "AgbFirmware", "Firmware", "Firmware", "Normal", "None", "None", "None"] 5 | crypt_type: [1, 2, 2, 2, 3, 0, 0, 0] 6 | partitions_offset: [0, 185597952, 185794560, 189988864, 194183168] 7 | partitions_size: [185597952, 196608, 4194304, 4194304, 1106051072] 8 | available_partitions: ["firm0", "firm1", "firm2", "firm3", "firm4"] 9 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/ncch.txt: -------------------------------------------------------------------------------- 1 | # Manually created and committed 2 | fake_ncch.yml,fake.ncsd,16384,3072 3 | 4 | # Optionals not committed 5 | aofm_program.yml,aofm.cia,14656,186109952 6 | aofm_manual.yml,aofm.cia,186124608,409600 7 | update_program.yml,update.csu,16384,148090880 8 | pwaat_program.yml,pwaat.cia,14656,384299008 9 | pwaat_manual.yml,pwaat.cia,384313664,884736 10 | pwdd_program.yml,pwdd.cia,14656,578580480 11 | pwdd_manual.yml,pwdd.cia,578595136,1966080 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.rulers": [ 3 | 80, 4 | 120 5 | ], 6 | "editor.renderWhitespace": "boundary", 7 | "editor.defaultFormatter": "esbenp.prettier-vscode", 8 | "[markdown]": { 9 | "editor.formatOnSave": true, 10 | }, 11 | "[csharp]": { 12 | "editor.defaultFormatter": "ms-dotnettools.csharp", 13 | "editor.formatOnType": true 14 | }, 15 | "cSpell.words": [ 16 | "IVFC", 17 | "NAND", 18 | "NCCH", 19 | "NCSD" 20 | ] 21 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Attach", 9 | "type": "coreclr", 10 | "request": "attach", 11 | "processId": "${command:pickProcess}", 12 | "justMyCode": false, 13 | "enableStepFiltering": false 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | ## Goal 10 | 11 | TODO: describe with user stories or a short text the goal of the feature. 12 | 13 | ## Description 14 | 15 | TODO: describe the motivation behind of the idea and what it should do. 16 | 17 | ## Proposed solution 18 | 19 | TODO: add any ideas for the implementation or how it should look like. 20 | 21 | ## Acceptance criteria 22 | 23 | TODO: list of expectations it should pass to close the request. 24 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the latest stable version is actively supported with critical bug fixed and 6 | security issues. 7 | 8 | ## Reporting a Vulnerability 9 | 10 | Vulnerabilities can be reported to my personal email address (you can check it 11 | from my [GitHub profile](https://github.com/pleonex)). 12 | 13 | All the security issues will be analyzed and a reply will be given in two 14 | working days. Once the issue is accepted it will be fixed in the current 15 | development branch and for the latest version. A new version would be released. 16 | -------------------------------------------------------------------------------- /src/Lemon/LoggerFactory.cs: -------------------------------------------------------------------------------- 1 | namespace SceneGate.Lemon; 2 | 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Logging.Abstractions; 5 | 6 | /// 7 | /// Logger factory for the framework. 8 | /// 9 | public static class LoggerFactory 10 | { 11 | private static ILoggerFactory factory; 12 | 13 | /// 14 | /// Gets or sets the logger factory to use for the framework. 15 | /// 16 | public static ILoggerFactory Instance { 17 | get => factory ??= NullLoggerFactory.Instance; 18 | set => factory = value; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | ## Description 10 | 11 | TODO: describe the issue 12 | 13 | ## Reproducer 14 | 15 | TODO: steps to reproduce the behavior. 16 | 17 | ## Expected behavior 18 | 19 | TODO: description of the expected behavior. 20 | 21 | ## Report info 22 | 23 | TODO: if applicable, the full exception stacktrace that you get. 24 | 25 | TODO: if applicable, add screenshots to help explain your problem. 26 | 27 | TODO: describe your environment like OS, app/lib version 28 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/titles/aofm_tmd.yml: -------------------------------------------------------------------------------- 1 | sign_type: 65540 2 | valid_signature: false 3 | signature_issuer: Root-CA00000003-CP0000000b 4 | version: 1 5 | ca_crl_version: 0 6 | signer_crl_version: 0 7 | system_version: 0 8 | title_id: 1125899907790080 9 | title_type: 64 10 | group_id: 0 11 | save_size: 2048 12 | srl_private_save_size: 0 13 | srl_flag: 0 14 | access_rights: 0 15 | title_version: 0 16 | boot_content: 0 17 | info_records: 18 | - command_count: 2 19 | index_offset: 0 20 | chunks: 21 | - id: 0 22 | index: 0 23 | attributes: None 24 | size: 186109952 25 | - id: 1 26 | index: 1 27 | attributes: None 28 | size: 409600 29 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | TODO: description of the PR work. 2 | 3 | This PR closes # 4 | 5 | ## Quality check list 6 | 7 | - [ ] Related code has been tested automatically or manually 8 | - [ ] Related documentation is updated 9 | - [ ] I acknowledge I have read and filled this checklist and accept the 10 | [developer certificate of origin](https://developercertificate.org/) 11 | 12 | ## Acceptance criteria 13 | 14 | TODO: list of expectations it has passed from the related issue. 15 | 16 | ## Follow-up work 17 | 18 | TODO: describe any missing or future required work. 19 | 20 | ## Example 21 | 22 | TODO: small code-snippet or screenshot of the work 23 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/titles/aofm_tmd_legit.yml: -------------------------------------------------------------------------------- 1 | sign_type: 65540 2 | valid_signature: false 3 | signature_issuer: Root-CA00000003-CP0000000b 4 | version: 1 5 | ca_crl_version: 0 6 | signer_crl_version: 0 7 | system_version: 0 8 | title_id: 1125899907790080 9 | title_type: 64 10 | group_id: 0 11 | save_size: 2048 12 | srl_private_save_size: 0 13 | srl_flag: 0 14 | access_rights: 0 15 | title_version: 0 16 | boot_content: 0 17 | info_records: 18 | - command_count: 2 19 | index_offset: 0 20 | chunks: 21 | - id: 0 22 | index: 0 23 | attributes: Encrypted 24 | size: 186109952 25 | - id: 1 26 | index: 1 27 | attributes: Encrypted 28 | size: 409600 29 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/fake_exefs.yml: -------------------------------------------------------------------------------- 1 | name: NodeContainerRoot 2 | format_type: null 3 | tags: 4 | check_children: true 5 | children: 6 | - name: .code 7 | format_type: Yarhl.IO.BinaryFormat 8 | stream_offset: 17408 9 | stream_length: 32 10 | tags: 11 | check_children: true 12 | children: 13 | 14 | - name: banner 15 | format_type: Yarhl.IO.BinaryFormat 16 | stream_offset: 17920 17 | stream_length: 64 18 | tags: 19 | check_children: true 20 | children: 21 | 22 | - name: randomm 23 | format_type: Yarhl.IO.BinaryFormat 24 | stream_offset: 18432 25 | stream_length: 80 26 | tags: 27 | check_children: true 28 | children: 29 | -------------------------------------------------------------------------------- /src/Tests.runsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | cobertura 10 | [.*UnitTest]*,[.*IntegrationTests]* 11 | GeneratedCodeAttribute,ExcludeFromCodeCoverageAttribute 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/aofm_exefs.yml: -------------------------------------------------------------------------------- 1 | name: NodeContainerRoot 2 | format_type: null 3 | tags: 4 | check_children: true 5 | children: 6 | - name: .code 7 | format_type: Yarhl.IO.BinaryFormat 8 | stream_offset: 18240 9 | stream_length: 980732 10 | tags: 11 | check_children: true 12 | children: 13 | 14 | - name: banner 15 | format_type: Yarhl.IO.BinaryFormat 16 | stream_offset: 999232 17 | stream_length: 165064 18 | tags: 19 | check_children: true 20 | children: 21 | 22 | - name: icon 23 | format_type: Yarhl.IO.BinaryFormat 24 | stream_offset: 1164608 25 | stream_length: 14016 26 | tags: 27 | check_children: true 28 | children: 29 | 30 | - name: logo 31 | format_type: Yarhl.IO.BinaryFormat 32 | stream_offset: 1178944 33 | stream_length: 8192 34 | tags: 35 | check_children: true 36 | children: 37 | -------------------------------------------------------------------------------- /.config/dotnet-tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "isRoot": true, 4 | "tools": { 5 | "gitversion.tool": { 6 | "version": "5.12.0", 7 | "commands": [ 8 | "dotnet-gitversion" 9 | ], 10 | "rollForward": false 11 | }, 12 | "thirdlicense": { 13 | "version": "1.3.1", 14 | "commands": [ 15 | "thirdlicense" 16 | ], 17 | "rollForward": false 18 | }, 19 | "dotnet-reportgenerator-globaltool": { 20 | "version": "5.2.0", 21 | "commands": [ 22 | "reportgenerator" 23 | ], 24 | "rollForward": false 25 | }, 26 | "docfx": { 27 | "version": "2.77.0", 28 | "commands": [ 29 | "docfx" 30 | ], 31 | "rollForward": false 32 | }, 33 | "gitreleasemanager.tool": { 34 | "version": "0.16.0", 35 | "commands": [ 36 | "dotnet-gitreleasemanager" 37 | ], 38 | "rollForward": false 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "process", 6 | "command": "dotnet", 7 | "args": [ 8 | "run", 9 | "--project", 10 | "build/orchestrator/", 11 | ], 12 | "group": { 13 | "kind": "build", 14 | "isDefault": true 15 | }, 16 | "problemMatcher": ["$msCompile"], 17 | "label": "Build" 18 | }, 19 | { 20 | "type": "process", 21 | "command": "dotnet", 22 | "args": [ 23 | "run", 24 | "--project", 25 | "build/orchestrator/", 26 | "--", 27 | "--target=Bundle" 28 | ], 29 | "group": { 30 | "kind": "build", 31 | }, 32 | "problemMatcher": ["$msCompile"], 33 | "label": "Bundle" 34 | }, 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/Lemon/Lemon.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SceneGate.Lemon 5 | Library for Nintendo 3DS file formats. 6 | true 7 | 8 | net6.0;net8.0 9 | 10 | SceneGate.Lemon 11 | enable 12 | disable 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /docs/template/public/main.css: -------------------------------------------------------------------------------- 1 | /* Changing the site font */ 2 | @import url("https://fonts.googleapis.com/css2?family=Nunito:wght@100;400;700&display=swap"); 3 | @import url("https://fonts.googleapis.com/css2?family=Fira Code&display=swap"); 4 | 5 | :root { 6 | --bs-font-sans-serif: "Nunito"; 7 | --bs-font-monospace: "Fira Code"; 8 | } 9 | 10 | /* Hide breadcrum bar on large screen as it only links to itself */ 11 | @media (min-width: 768px) { 12 | .actionbar { 13 | display: none !important; 14 | } 15 | } 16 | 17 | /* Give more space for section separation in a doc */ 18 | h2 { 19 | margin-top: 2rem !important; 20 | } 21 | 22 | /* Improve TOC with a line for categories (entries without link) */ 23 | .toc span.name-only { 24 | border-bottom-color: var(--bs-tertiary-color) !important; 25 | border-bottom-width: 2px !important; 26 | border-bottom-style: solid !important; 27 | margin-bottom: 0 !important; 28 | margin-top: 0.6rem !important; 29 | } 30 | 31 | .toc span.name-only:first() { 32 | margin-top: 0.4rem !important; 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/build-and-release.yml: -------------------------------------------------------------------------------- 1 | name: Build and release 2 | 3 | on: 4 | # Dev 5 | workflow_dispatch: 6 | pull_request: 7 | push: 8 | # Preview 9 | branches: [ develop ] 10 | # Stable 11 | tags: [ "v*" ] 12 | 13 | jobs: 14 | build: 15 | name: "Build" 16 | uses: ./.github/workflows/build.yml 17 | with: 18 | dotnet_version: '8.0.401' 19 | 20 | # Preview release on push to develop only 21 | # Stable release on version tag push only 22 | deploy: 23 | name: "Deploy" 24 | if: github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/tags/v') 25 | needs: build 26 | uses: ./.github/workflows/deploy.yml 27 | with: 28 | dotnet_version: '8.0.401' 29 | azure_nuget_feed: 'https://pkgs.dev.azure.com/SceneGate/SceneGate/_packaging/SceneGate-Preview/nuget/v3/index.json' 30 | secrets: 31 | nuget_preview_token: "az" # Dummy values as we use Azure DevOps 32 | nuget_stable_token: "${{ secrets.NUGET_FEED_TOKEN }}" 33 | azure_nuget_token: ${{ secrets.ADO_NUGET_FEED_TOKEN }} 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 SceneGate 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 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Lemon.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SceneGate.Lemon.IntegrationTests 5 | Integration tests for Lemon. 6 | 7 | net6.0;net8.0 8 | 9 | SceneGate.Lemon.IntegrationTests 10 | false 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /GitReleaseManager.yaml: -------------------------------------------------------------------------------- 1 | # Configuration values used when creating new releases 2 | create: 3 | include-footer: false 4 | include-sha-section: false 5 | allow-update-to-published: false 6 | 7 | # Configuration values used when exporting release notes 8 | export: 9 | include-created-date-in-title: true 10 | created-date-string-format: MMMM dd, yyyy 11 | perform-regex-removal: false 12 | 13 | # Configuration values used when closing a milestone 14 | close: 15 | use-issue-comments: false 16 | # issue-comment: |- 17 | # :tada: This issue has been resolved in version {milestone} :tada: 18 | # 19 | # The release is available on: 20 | # 21 | # - [GitHub release](https://github.com/{owner}/{repository}/releases/tag/{milestone}) 22 | # 23 | # Your **[GitReleaseManager](https://github.com/GitTools/GitReleaseManager)** bot :package::rocket: 24 | 25 | # The labels that will be used to include issues in release notes. 26 | issue-labels-include: 27 | - Breaking 28 | - Bug 29 | - Duplicate 30 | - Documentation 31 | - Enhancement 32 | - Feature 33 | - Improvement 34 | - Question 35 | 36 | # The labels that will NOT be used when including issues in release notes. 37 | issue-labels-exclude: 38 | - Internal Refactoring 39 | 40 | # Overrides default pluralization and header names for specific labels. 41 | issue-labels-alias: 42 | - name: Documentation 43 | header: Documentation 44 | plural: Documentation 45 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/aofm_cia.yml: -------------------------------------------------------------------------------- 1 | name: NodeContainerRoot 2 | format_type: null 3 | tags: 4 | version: 0 5 | type: 0 6 | check_children: true 7 | children: 8 | - name: certs_chain 9 | format_type: Yarhl.IO.BinaryFormat 10 | stream_offset: 0x2040 11 | stream_length: 0x0A00 12 | tags: 13 | check_children: true 14 | children: 15 | 16 | - name: ticket 17 | format_type: Yarhl.IO.BinaryFormat 18 | stream_offset: 0x2A40 19 | stream_length: 0x0350 20 | tags: 21 | check_children: true 22 | children: 23 | 24 | - name: title 25 | format_type: Yarhl.IO.BinaryFormat 26 | stream_offset: 0x2DC0 27 | stream_length: 0x0B64 28 | tags: 29 | check_children: true 30 | children: 31 | 32 | - name: content 33 | format_type: Yarhl.FileSystem.NodeContainerFormat 34 | tags: 35 | check_children: true 36 | children: 37 | - name: program 38 | format_type: Yarhl.IO.BinaryFormat 39 | stream_offset: 0x3940 40 | stream_length: 0x0B17D000 41 | tags: 42 | check_children: true 43 | children: 44 | 45 | - name: manual 46 | format_type: Yarhl.IO.BinaryFormat 47 | stream_offset: 0x0B180940 48 | stream_length: 0x064000 49 | tags: 50 | check_children: true 51 | children: 52 | 53 | - name: metadata 54 | format_type: Yarhl.IO.BinaryFormat 55 | stream_offset: 0x0B1E4940 56 | stream_length: 0x3AC0 57 | tags: 58 | check_children: true 59 | children: 60 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/aofm_cia_legit.yml: -------------------------------------------------------------------------------- 1 | name: NodeContainerRoot 2 | format_type: null 3 | tags: 4 | version: 0 5 | type: 0 6 | check_children: true 7 | children: 8 | - name: certs_chain 9 | format_type: Yarhl.IO.BinaryFormat 10 | stream_offset: 0x2040 11 | stream_length: 0x0A00 12 | tags: 13 | check_children: true 14 | children: 15 | 16 | - name: ticket 17 | format_type: Yarhl.IO.BinaryFormat 18 | stream_offset: 0x2A40 19 | stream_length: 0x0350 20 | tags: 21 | check_children: true 22 | children: 23 | 24 | - name: title 25 | format_type: Yarhl.IO.BinaryFormat 26 | stream_offset: 0x2DC0 27 | stream_length: 0x0B64 28 | tags: 29 | check_children: true 30 | children: 31 | 32 | - name: content 33 | format_type: Yarhl.FileSystem.NodeContainerFormat 34 | tags: 35 | check_children: true 36 | children: 37 | - name: program 38 | format_type: Yarhl.IO.BinaryFormat 39 | stream_offset: 0x3940 40 | stream_length: 0x0B17D000 41 | tags: 42 | LEMON_NCCH_ENCRYPTED: True 43 | check_children: true 44 | children: 45 | 46 | - name: manual 47 | format_type: Yarhl.IO.BinaryFormat 48 | stream_offset: 0x0B180940 49 | stream_length: 0x064000 50 | tags: 51 | LEMON_NCCH_ENCRYPTED: True 52 | check_children: true 53 | children: 54 | 55 | - name: metadata 56 | format_type: Yarhl.IO.BinaryFormat 57 | stream_offset: 0x0B1E4940 58 | stream_length: 0x3AC0 59 | tags: 60 | check_children: true 61 | children: 62 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Extensions/FluentAssertionsExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.IntegrationTests.Titles; 21 | 22 | using System.Text.RegularExpressions; 23 | using FluentAssertions.Equivalency; 24 | 25 | public static class FluentAssertionsExtensions 26 | { 27 | public static string RemovingIndexes(this IMemberInfo info) 28 | { 29 | // By LuisMiguelFilipe: https://github.com/fluentassertions/fluentassertions/issues/500#issuecomment-472424542 30 | return Regex.Replace(info.Path, @"\[\d+\]", string.Empty); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Containers/NcchTestInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.IntegrationTests.Containers 21 | { 22 | public class NcchTestInfo 23 | { 24 | public int SignatureLength { 25 | get; 26 | set; 27 | } 28 | 29 | public int[] RegionsOffset { 30 | get; 31 | set; 32 | } 33 | 34 | public int[] RegionsSize { 35 | get; 36 | set; 37 | } 38 | 39 | public string[] AvailableRegions { 40 | get; 41 | set; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Lemon/Containers/Formats/FirmwareType.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.Containers.Formats 21 | { 22 | /// 23 | /// File system type. 24 | /// 25 | public enum FirmwareType 26 | { 27 | /// 28 | /// Regular partition. 29 | /// 30 | None = 0, 31 | 32 | /// 33 | /// DS(i) firmware partition. 34 | /// 35 | Normal = 1, 36 | 37 | /// 38 | /// Firmware partition. 39 | /// 40 | Firmware = 3, 41 | 42 | /// 43 | /// GBA Firmware partition. 44 | /// 45 | AgbFirmware = 4, 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/docfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "src": [ 5 | { 6 | "files": [ 7 | "Lemon/*.csproj" 8 | ], 9 | "src": "../src" 10 | } 11 | ], 12 | "dest": "api", 13 | "includePrivateMembers": false, 14 | "disableGitFeatures": false, 15 | "disableDefaultFilter": false, 16 | "noRestore": false, 17 | "namespaceLayout": "flattened", 18 | "memberLayout": "samePage", 19 | "EnumSortOrder": "alphabetic", 20 | "allowCompilationErrors": false 21 | } 22 | ], 23 | "build": { 24 | "content": [ 25 | { 26 | "files": [ 27 | "api/**.yml", 28 | "api/index.md" 29 | ] 30 | }, 31 | { "files": "**/*.{md,yml}", "src": "articles", "dest": "docs" }, 32 | { "files": [ "toc.yml", "*.md" ] } 33 | ], 34 | "resource": [ 35 | { 36 | "files": [ "**/images/**", "**/resources/**" ], 37 | "exclude": [ "_site/**", "obj/**" ] 38 | } 39 | ], 40 | "output": "_site", 41 | "globalMetadata": { 42 | "_appTitle": "Lemon - 3DS formats for .NET", 43 | "_appName": "Lemon", 44 | "_appFooter": "Copyright (c) 2019 SceneGate. Docs made with docfx", 45 | "_appLogoPath": "images/logo-48.png", 46 | "_appFaviconPath": "images/favicon.png", 47 | "_enableSearch": true, 48 | "_enableNewTab": true, 49 | "_lang": "en" 50 | }, 51 | "fileMetadataFiles": [], 52 | "template": [ 53 | "default", 54 | "modern", 55 | "template" 56 | ], 57 | "postProcessors": [], 58 | "keepFileLink": false, 59 | "disableGitFeatures": false, 60 | "sitemap": { 61 | "baseUrl": "https://scenegate.github.io/Lemon", 62 | "priority": 0.5, 63 | "changefreq": "monthly" 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/Lemon/Containers/Converters/Ivfc/BlockWrittenEventArgs.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.Containers.Converters.Ivfc 21 | { 22 | /// 23 | /// Event argument for written blocks of data. 24 | /// 25 | internal class BlockWrittenEventArgs 26 | { 27 | /// 28 | /// Initializes a new instance of the 29 | /// class. 30 | /// 31 | /// The hash to pass in the argument. 32 | public BlockWrittenEventArgs(byte[] hash) 33 | { 34 | Hash = hash; 35 | } 36 | 37 | /// 38 | /// Gets the hash of the written block. 39 | /// 40 | public byte[] Hash { get; } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: "Build" 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | dotnet_version: 7 | required: true 8 | type: string 9 | 10 | jobs: 11 | build: 12 | strategy: 13 | matrix: 14 | os: [ ubuntu-latest, macos-latest, windows-latest ] 15 | include: 16 | # By default they are no "main build" but if it matches "os" then yes. 17 | - os: ubuntu-latest 18 | is_main_build: true 19 | name: "${{ matrix.os }}" 20 | runs-on: ${{ matrix.os }} 21 | steps: 22 | - name: "Checkout" 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 # We need full history for version number 26 | lfs: true 27 | 28 | - name: "Setup .NET SDK" 29 | uses: actions/setup-dotnet@v4 30 | with: 31 | dotnet-version: ${{ inputs.dotnet_version }} 32 | 33 | - name: "Setup .NET 6.0 SDK" 34 | uses: actions/setup-dotnet@v4 35 | with: 36 | dotnet-version: '6.0.425' 37 | 38 | - name: "Build and test" 39 | run: dotnet run --project build/orchestrator -- --target=Default --dotnet-configuration=Release 40 | env: 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | 43 | - name: "Bundle" 44 | if: ${{ matrix.is_main_build }} 45 | run: dotnet run --project build/orchestrator -- --target=Bundle --dotnet-configuration=Release 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | 49 | - name: "Publish artifacts to CI" 50 | if: ${{ matrix.is_main_build }} 51 | uses: actions/upload-artifact@v4 52 | with: 53 | name: "Artifacts" 54 | retention-days: 7 55 | path: | 56 | build/artifacts/ 57 | !build/artifacts/docs 58 | 59 | - name: Publish docs artifact to CI 60 | if: ${{ matrix.is_main_build }} 61 | uses: actions/upload-pages-artifact@v3 62 | with: 63 | path: build/artifacts/docs 64 | -------------------------------------------------------------------------------- /src/Lemon/Containers/Formats/Ncch.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.Containers.Formats 21 | { 22 | using Yarhl.FileSystem; 23 | 24 | /// 25 | /// Nintendo Content Container Header. 26 | /// This is the format for the CXI and CFA specialization. 27 | /// It can contain up to two file systems and several special files. 28 | /// 29 | public class Ncch : NodeContainerFormat 30 | { 31 | /// 32 | /// Initializes a new instance of the class. 33 | /// 34 | public Ncch() 35 | { 36 | Header = new NcchHeader(); 37 | } 38 | 39 | /// 40 | /// Gets or sets the header. 41 | /// 42 | /// The header. 43 | public NcchHeader Header { 44 | get; 45 | set; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Lemon [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://choosealicense.com/licenses/mit/) 2 | 3 | _Lemon_ is a library part of the [_SceneGate_](https://github.com/SceneGate) 4 | framework that provides support for **3DS file formats.** 5 | 6 | ## Supported formats 7 | 8 | _Encryption, decryption or signature validation not supported yet._ 9 | 10 | - **NCSD (CCI and CSU)**: unpack 11 | - **CIA**: unpack and pack 12 | - **NCCH (CXI and CFA)**: unpack and pack 13 | - **ExeFS**: unpack and pack 14 | - **RomFS**: unpack and pack 15 | - **TMD**: deserialize 16 | 17 | ## Usage 18 | 19 | The project provides the following .NET libraries (NuGet packages in nuget.org). 20 | The libraries works on supported versions of .NET: 6.0 and 8.0. 21 | 22 | - [![SceneGate.Lemon](https://img.shields.io/nuget/v/SceneGate.Lemon?label=SceneGate.Lemon&logo=nuget)](https://www.nuget.org/packages/SceneGate.Lemon): 23 | support for 3DS formats 24 | 25 | Preview releases can be found in this 26 | [Azure DevOps package repository](https://dev.azure.com/SceneGate/SceneGate/_packaging?_a=feed&feed=SceneGate-Preview). 27 | To use a preview release, create a file `nuget.config` in the same directory of 28 | your solution file (.sln) with the following content: 29 | 30 | ```xml 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ``` 50 | 51 | ## References 52 | 53 | - [3D Brew](https://www.3dbrew.org/wiki/Main_Page) 54 | -------------------------------------------------------------------------------- /src/Lemon/Titles/ContentAttributes.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.Titles 21 | { 22 | using System; 23 | 24 | /// 25 | /// Attributes for a partition (NCCH) from the title metadata. 26 | /// 27 | [Flags] 28 | public enum ContentAttributes { 29 | /// 30 | /// No special attributes. 31 | /// 32 | None = 0, 33 | 34 | /// 35 | /// The content is encrypted. 36 | /// 37 | Encrypted = 1, 38 | 39 | /// 40 | /// Unknown. The content comes from a cartridge? 41 | /// 42 | Disc = 2, 43 | 44 | /// 45 | /// Unknown. 46 | /// 47 | Cfm = 4, 48 | 49 | /// 50 | /// The content is optional. 51 | /// 52 | Optional = 0x4000, 53 | 54 | /// 55 | /// The content is shared. 56 | /// 57 | Shared = 0x8000, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Lemon/Titles/ContentInfoRecord.cs: -------------------------------------------------------------------------------- 1 | // ContentInfoRecord.cs 2 | // 3 | // Author: 4 | // Maxwell Ruiz maxwellaquaruiz@gmail.com 5 | // 6 | // Copyright (c) 2022 Maxwell Ruiz 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | namespace SceneGate.Lemon.Titles 26 | { 27 | /// 28 | /// Information about a CIA content info record. 29 | /// 30 | public class ContentInfoRecord 31 | { 32 | /// 33 | /// Gets or sets the content index offset. 34 | /// 35 | public short IndexOffset { get; set; } 36 | 37 | /// 38 | /// Gets or sets the content command count (number of chunks to hash). 39 | /// 40 | public short CommandCount { get; set; } 41 | 42 | /// 43 | /// Gets or sets the record hash. 44 | /// 45 | public byte[] Hash { get; set; } 46 | 47 | /// 48 | /// Gets or sets a value indicating whether if the info record is empty. 49 | /// 50 | public bool IsEmpty { get; set; } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Containers/NcsdTestInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.IntegrationTests.Containers 21 | { 22 | using SceneGate.Lemon.Containers.Formats; 23 | 24 | public class NcsdTestInfo 25 | { 26 | public int SignatureLength { 27 | get; 28 | set; 29 | } 30 | 31 | public long Size { 32 | get; 33 | set; 34 | } 35 | 36 | public ulong MediaId { 37 | get; 38 | set; 39 | } 40 | 41 | public byte[] CryptType { 42 | get; 43 | set; 44 | } 45 | 46 | public FirmwareType[] FirmwaresType { 47 | get; 48 | set; 49 | } 50 | 51 | public uint[] PartitionsOffset { 52 | get; 53 | set; 54 | } 55 | 56 | public uint[] PartitionsSize { 57 | get; 58 | set; 59 | } 60 | 61 | public string[] AvailablePartitions { 62 | get; 63 | set; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Lemon/Containers/Formats/Ncsd.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.Containers.Formats 21 | { 22 | using Yarhl.FileSystem; 23 | 24 | /// 25 | /// Nintendo CTR SD. 26 | /// This is the format for the CCI, NAND and CSU specializations. 27 | /// It can contains up to 8 containers / nodes. 28 | /// 29 | public class Ncsd : NodeContainerFormat 30 | { 31 | /// 32 | /// Initializes a new instance of the class. 33 | /// 34 | public Ncsd() 35 | { 36 | Header = new NcsdHeader(); 37 | } 38 | 39 | /// 40 | /// Gets the maximum number of partitions on a Ncsd format. 41 | /// 42 | /// The maximum number of partitions. 43 | public static int NumPartitions { 44 | get { return 8; } 45 | } 46 | 47 | /// 48 | /// Gets or sets the header. 49 | /// 50 | /// The header. 51 | public NcsdHeader Header { 52 | get; 53 | set; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Containers/NodeContainerInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.IntegrationTests.Containers 21 | { 22 | using System.Collections.Generic; 23 | using System.Collections.ObjectModel; 24 | using System.IO; 25 | using YamlDotNet.Serialization; 26 | using YamlDotNet.Serialization.NamingConventions; 27 | 28 | public class NodeContainerInfo 29 | { 30 | public string Name { get; set; } 31 | 32 | public string FormatType { get; set; } 33 | 34 | public long StreamOffset { get; set; } 35 | 36 | public long StreamLength { get; set; } 37 | 38 | public Dictionary Tags { get; set; } 39 | 40 | public bool CheckChildren { get; set; } 41 | 42 | public Collection Children { get; set; } 43 | 44 | public static NodeContainerInfo FromYaml(string path) 45 | { 46 | string yaml = File.ReadAllText(path); 47 | return new DeserializerBuilder() 48 | .WithNamingConvention(UnderscoredNamingConvention.Instance) 49 | .Build() 50 | .Deserialize(yaml); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Titles/TestData.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.IntegrationTests.Titles 21 | { 22 | using System.Collections; 23 | using System.IO; 24 | using System.Linq; 25 | using NUnit.Framework; 26 | 27 | public static class TestData 28 | { 29 | public static IEnumerable TmdParams { 30 | get => GetSubstreamAndInfoCollection("tmd.txt"); 31 | } 32 | 33 | public static string ResourceDirectory { 34 | get => Path.Combine(TestDataBase.RootFromOutputPath, "titles"); 35 | } 36 | 37 | private static IEnumerable GetSubstreamAndInfoCollection(string listName) 38 | { 39 | return TestDataBase.ReadTestListFile(Path.Combine(ResourceDirectory, listName)) 40 | .Select(line => line.Split(',')) 41 | .Select(data => new TestFixtureData( 42 | Path.Combine(ResourceDirectory, data[0]), 43 | Path.Combine(ResourceDirectory, data[1]), 44 | int.Parse(data[2]), 45 | int.Parse(data[3])) 46 | .SetArgDisplayNames(data[0], data[1])); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: "Deploy" 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | dotnet_version: 7 | required: true 8 | type: string 9 | azure_nuget_feed: 10 | required: false 11 | type: string 12 | secrets: 13 | nuget_preview_token: 14 | required: false 15 | nuget_stable_token: 16 | required: false 17 | azure_nuget_token: 18 | required: false 19 | 20 | jobs: 21 | upload_doc: 22 | name: "Documentation" 23 | runs-on: "ubuntu-latest" 24 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 25 | permissions: 26 | pages: write # to deploy to Pages 27 | id-token: write # to verify the deployment originates from an appropriate source 28 | # Deploy to the github-pages environment 29 | environment: 30 | name: github-pages 31 | url: ${{ steps.deployment.outputs.page_url }} 32 | steps: 33 | - name: Deploy to GitHub Pages 34 | id: deployment 35 | uses: actions/deploy-pages@v4 36 | 37 | push_artifacts: 38 | name: "Artifacts" 39 | runs-on: "ubuntu-latest" 40 | steps: 41 | - name: "Checkout" 42 | uses: actions/checkout@v4 43 | with: 44 | fetch-depth: 0 # We need full history for version number 45 | 46 | - name: "Download artifacts" 47 | uses: actions/download-artifact@v4 48 | with: 49 | name: "Artifacts" 50 | path: "./build/artifacts/" 51 | 52 | - name: "Setup .NET SDK" 53 | uses: actions/setup-dotnet@v4 54 | with: 55 | dotnet-version: ${{ inputs.dotnet_version }} 56 | 57 | # Weird way to authenticate in Azure DevOps Artifacts 58 | # Then, we need to setup VSS_NUGET_EXTERNAL_FEED_ENDPOINTS 59 | - name: "Install Azure Artifacts Credential Provider" 60 | run: wget -qO- https://aka.ms/install-artifacts-credprovider.sh | bash 61 | 62 | - name: "Deploy artifacts" 63 | run: dotnet run --project build/orchestrator -- --target=Deploy 64 | env: 65 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 66 | STABLE_NUGET_FEED_TOKEN: ${{ secrets.nuget_stable_token }} 67 | PREVIEW_NUGET_FEED_TOKEN: ${{ secrets.nuget_preview_token }} 68 | VSS_NUGET_EXTERNAL_FEED_ENDPOINTS: '{"endpointCredentials": [{"endpoint":"${{ inputs.azure_nuget_feed }}", "username":"", "password":"${{ secrets.azure_nuget_token }}"}]}' 69 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | SceneGate 4 | scenegate 5 | None 6 | Copyright (C) 2019 SceneGate 7 | 8 | true 9 | 10 | 12 | false 13 | 14 | 15 | 16 | MIT 17 | https://scenegate.github.io/Lemon/ 18 | https://github.com/SceneGate/Lemon 19 | icon.png 20 | romhacking;3ds;scenegate 21 | README.md 22 | 23 | 24 | 25 | 26 | 27 | true 28 | 29 | 30 | true 31 | 32 | 38 | embedded 39 | 40 | 41 | true 42 | true 43 | 44 | 45 | 46 | 47 | true 48 | true 49 | latest 50 | true 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Resources/containers/aofm_ivfc.yml: -------------------------------------------------------------------------------- 1 | name: NodeContainerRoot 2 | format_type: null 3 | tags: 4 | check_children: true 5 | children: 6 | - name: gkk 7 | format_type: Yarhl.FileSystem.NodeContainerFormat 8 | tags: 9 | check_children: false 10 | children: 11 | - name: lv5 12 | format_type: Yarhl.FileSystem.NodeContainerFormat 13 | tags: 14 | check_children: true 15 | children: 16 | - name: fix.xr 17 | format_type: Yarhl.IO.BinaryFormat 18 | stream_offset: 0xA12D3F0 19 | stream_length: 24592 20 | tags: 21 | check_children: true 22 | children: 23 | - name: chr 24 | format_type: Yarhl.FileSystem.NodeContainerFormat 25 | tags: 26 | check_children: true 27 | children: 28 | - name: c100.xc 29 | format_type: Yarhl.IO.BinaryFormat 30 | stream_offset: 169030656 31 | stream_length: 281444 32 | tags: 33 | check_children: true 34 | children: 35 | - name: c101.xc 36 | format_type: Yarhl.IO.BinaryFormat 37 | stream_offset: 169312112 38 | stream_length: 283876 39 | tags: 40 | check_children: true 41 | children: 42 | - name: c102.xc 43 | format_type: Yarhl.IO.BinaryFormat 44 | stream_offset: 169596000 45 | stream_length: 314324 46 | tags: 47 | check_children: true 48 | children: 49 | - name: c103.xc 50 | format_type: Yarhl.IO.BinaryFormat 51 | stream_offset: 169910336 52 | stream_length: 350356 53 | tags: 54 | check_children: true 55 | children: 56 | - name: fnt 57 | format_type: Yarhl.FileSystem.NodeContainerFormat 58 | tags: 59 | check_children: false 60 | children: 61 | - name: menu 62 | format_type: Yarhl.FileSystem.NodeContainerFormat 63 | tags: 64 | check_children: false 65 | children: 66 | - name: mov 67 | format_type: Yarhl.FileSystem.NodeContainerFormat 68 | tags: 69 | check_children: false 70 | children: 71 | - name: seq 72 | format_type: Yarhl.FileSystem.NodeContainerFormat 73 | tags: 74 | check_children: false 75 | children: 76 | - name: sky 77 | format_type: Yarhl.FileSystem.NodeContainerFormat 78 | tags: 79 | check_children: false 80 | children: 81 | - name: snd 82 | format_type: Yarhl.FileSystem.NodeContainerFormat 83 | tags: 84 | check_children: false 85 | children: 86 | -------------------------------------------------------------------------------- /src/Lemon/Containers/ContainerManager.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.Containers 21 | { 22 | using System; 23 | using SceneGate.Lemon.Containers.Converters; 24 | using Yarhl.FileSystem; 25 | using Yarhl.IO; 26 | 27 | /// 28 | /// Manage containers for 3DS formats. 29 | /// 30 | public static class ContainerManager 31 | { 32 | /// 33 | /// Unpack a node with a binary format representing a .3ds file. 34 | /// 35 | /// Node to unpack with .3ds format. 36 | public static void Unpack3DSNode(Node gameNode) 37 | { 38 | if (gameNode == null) 39 | throw new ArgumentNullException(nameof(gameNode)); 40 | 41 | if (!(gameNode.Format is IBinary)) 42 | throw new ArgumentException("Invalid node format", nameof(gameNode)); 43 | 44 | // First transform the binary format into NCSD (CCI/CSU/NAND). 45 | gameNode.TransformWith(); 46 | 47 | // All the partition have NCCH format. 48 | foreach (var partition in gameNode.Children) { 49 | partition.TransformWith(); 50 | } 51 | 52 | // Unpack each partition until we have the actual file system. 53 | gameNode.Children["program"].Children["rom"] 54 | .TransformWith(); 55 | gameNode.Children["program"].Children["system"] 56 | .TransformWith(); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Containers/CiaConverterTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.IntegrationTests.Containers 21 | { 22 | using System.IO; 23 | using NUnit.Framework; 24 | using SceneGate.Lemon.Containers.Converters; 25 | using Yarhl.FileFormat; 26 | using Yarhl.FileSystem; 27 | using Yarhl.IO; 28 | 29 | [TestFixtureSource(typeof(TestData), nameof(TestData.CiaParams))] 30 | public class CiaConverterTests : Binary2ContainerTests 31 | { 32 | readonly string yamlPath; 33 | readonly string binaryPath; 34 | 35 | public CiaConverterTests(string yamlPath, string binaryPath) 36 | { 37 | this.yamlPath = yamlPath; 38 | this.binaryPath = binaryPath; 39 | 40 | TestDataBase.IgnoreIfFileDoesNotExist(binaryPath); 41 | TestDataBase.IgnoreIfFileDoesNotExist(yamlPath); 42 | } 43 | 44 | protected override BinaryFormat GetBinary() 45 | { 46 | TestContext.WriteLine($"{nameof(CiaConverterTests)}: {Path.GetFileName(binaryPath)}"); 47 | var stream = DataStreamFactory.FromFile(binaryPath, FileOpenMode.Read); 48 | return new BinaryFormat(stream); 49 | } 50 | 51 | protected override NodeContainerInfo GetContainerInfo() 52 | { 53 | return NodeContainerInfo.FromYaml(yamlPath); 54 | } 55 | 56 | protected override IConverter GetToContainerConverter() 57 | { 58 | return new BinaryCia2NodeContainer(); 59 | } 60 | 61 | protected override IConverter GetToBinaryConverter() 62 | { 63 | return new NodeContainer2BinaryCia(); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Lemon/Titles/ContentChunkRecord.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.Titles 21 | { 22 | using System; 23 | using System.Diagnostics.CodeAnalysis; 24 | using YamlDotNet.Serialization; 25 | 26 | /// 27 | /// Information about a CIA content chunk. 28 | /// 29 | public class ContentChunkRecord 30 | { 31 | /// 32 | /// Gets or sets the ID of the chunk. 33 | /// 34 | public int Id { get; set; } 35 | 36 | /// 37 | /// Gets or sets the chunk index which maps into its actual content. 38 | /// 39 | public short Index { get; set; } 40 | 41 | /// 42 | /// Gets or sets the type of content. 43 | /// 44 | public ContentAttributes Attributes { get; set; } 45 | 46 | /// 47 | /// Gets or sets the chunk size. 48 | /// 49 | public long Size { get; set; } 50 | 51 | /// 52 | /// Gets or sets the chunk hash. 53 | /// 54 | [SuppressMessage( 55 | "Performance", 56 | "CA1819", 57 | Justification = "This is how .NET API works with hashes too and this is a model")] 58 | [YamlIgnore] 59 | public byte[] Hash { get; set; } 60 | 61 | /// 62 | /// Gets the name of the chunk from its index. 63 | /// 64 | /// The chunk's name. 65 | public string GetChunkName() 66 | { 67 | return Index switch { 68 | 0 => "program", 69 | 1 => "manual", 70 | 2 => "download_play", 71 | _ => throw new NotSupportedException(), 72 | }; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/TestDataBase.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.IntegrationTests 21 | { 22 | using System; 23 | using System.Collections.Generic; 24 | using System.IO; 25 | using System.Linq; 26 | using NUnit.Framework; 27 | 28 | public static class TestDataBase 29 | { 30 | public static string RootFromOutputPath { 31 | get { 32 | string envVar = Environment.GetEnvironmentVariable("YARHL_TEST_DIR"); 33 | if (!string.IsNullOrEmpty(envVar)) 34 | return envVar; 35 | 36 | string programDir = AppDomain.CurrentDomain.BaseDirectory; 37 | string path = Path.Combine( 38 | programDir, 39 | "..", // framework 40 | "..", // configuration 41 | "..", // bin 42 | "Resources"); 43 | return Path.GetFullPath(path); 44 | } 45 | } 46 | 47 | public static void IgnoreIfFileDoesNotExist(string file) 48 | { 49 | if (!File.Exists(file)) { 50 | TestContext.Progress.WriteLine( 51 | "[{0}] Missing resource file: {1}", 52 | TestContext.CurrentContext.Test.ClassName, 53 | file); 54 | Assert.Ignore(); 55 | } 56 | } 57 | 58 | public static IEnumerable ReadTestListFile(string filePath) 59 | { 60 | if (!File.Exists(filePath)) { 61 | return Array.Empty(); 62 | } 63 | 64 | return File.ReadAllLines(filePath) 65 | .Where(line => !string.IsNullOrWhiteSpace(line)) 66 | .Select(line => line.Trim()) 67 | .Where(line => !line.StartsWith('#')); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Containers/IvfcConverterTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.IntegrationTests.Containers 21 | { 22 | using System.IO; 23 | using NUnit.Framework; 24 | using SceneGate.Lemon.Containers.Converters; 25 | using Yarhl.FileFormat; 26 | using Yarhl.FileSystem; 27 | using Yarhl.IO; 28 | 29 | [TestFixtureSource(typeof(TestData), nameof(TestData.IvfcParams))] 30 | public class IvfcConverterTests : Binary2ContainerTests 31 | { 32 | readonly string yamlPath; 33 | readonly string binaryPath; 34 | readonly int offset; 35 | readonly int size; 36 | 37 | public IvfcConverterTests(string yamlPath, string binaryPath, int offset, int size) 38 | { 39 | this.yamlPath = yamlPath; 40 | this.binaryPath = binaryPath; 41 | this.offset = offset; 42 | this.size = size; 43 | 44 | TestDataBase.IgnoreIfFileDoesNotExist(binaryPath); 45 | TestDataBase.IgnoreIfFileDoesNotExist(yamlPath); 46 | } 47 | 48 | protected override BinaryFormat GetBinary() 49 | { 50 | TestContext.WriteLine($"{nameof(IvfcConverterTests)}: {Path.GetFileName(binaryPath)}"); 51 | var stream = DataStreamFactory.FromFile(binaryPath, FileOpenMode.Read, offset, size); 52 | return new BinaryFormat(stream); 53 | } 54 | 55 | protected override NodeContainerInfo GetContainerInfo() 56 | { 57 | return NodeContainerInfo.FromYaml(yamlPath); 58 | } 59 | 60 | protected override IConverter GetToContainerConverter() 61 | { 62 | return new BinaryIvfc2NodeContainer(); 63 | } 64 | 65 | protected override IConverter GetToBinaryConverter() 66 | { 67 | return new NodeContainer2BinaryIvfc(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Containers/ExeFsConverterTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.IntegrationTests.Containers 21 | { 22 | using System.IO; 23 | using NUnit.Framework; 24 | using SceneGate.Lemon.Containers.Converters; 25 | using Yarhl.FileFormat; 26 | using Yarhl.FileSystem; 27 | using Yarhl.IO; 28 | 29 | [TestFixtureSource(typeof(TestData), nameof(TestData.ExeFsParams))] 30 | public class ExeFsConverterTests : Binary2ContainerTests 31 | { 32 | readonly string yamlPath; 33 | readonly string binaryPath; 34 | readonly int offset; 35 | readonly int size; 36 | 37 | public ExeFsConverterTests(string yamlPath, string binaryPath, int offset, int size) 38 | { 39 | this.yamlPath = yamlPath; 40 | this.binaryPath = binaryPath; 41 | this.offset = offset; 42 | this.size = size; 43 | 44 | TestDataBase.IgnoreIfFileDoesNotExist(binaryPath); 45 | TestDataBase.IgnoreIfFileDoesNotExist(yamlPath); 46 | } 47 | 48 | protected override BinaryFormat GetBinary() 49 | { 50 | TestContext.WriteLine($"{nameof(ExeFsConverterTests)}: {Path.GetFileName(binaryPath)}"); 51 | var stream = DataStreamFactory.FromFile(binaryPath, FileOpenMode.Read, offset, size); 52 | return new BinaryFormat(stream); 53 | } 54 | 55 | protected override NodeContainerInfo GetContainerInfo() 56 | { 57 | return NodeContainerInfo.FromYaml(yamlPath); 58 | } 59 | 60 | protected override IConverter GetToContainerConverter() 61 | { 62 | return new BinaryExeFs2NodeContainer(); 63 | } 64 | 65 | protected override IConverter GetToBinaryConverter() 66 | { 67 | return new BinaryExeFs2NodeContainer(); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Lemon/Containers/Converters/Ivfc/NameHash.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.Containers.Converters.Ivfc 21 | { 22 | /// 23 | /// Generate hashes for name and directory lookup. 24 | /// 25 | internal static class NameHash 26 | { 27 | /// 28 | /// Calculates the size of tokens for the hash table. 29 | /// 30 | /// The number of entries to hash. 31 | /// The number of tokens of the hash table. 32 | public static int CalculateTableLength(int numEntries) 33 | { 34 | if (numEntries < 3) { 35 | return 3; 36 | } else if (numEntries < 19) { 37 | // This will return 15 that it is not prime, but whatever Nin. 38 | return numEntries | 1; 39 | } 40 | 41 | // Find the first prime number with a simple algorithm. 42 | // Really lazy algorithm eh, Nin. 43 | bool IsMoreOrLessPrime(int x) => 44 | (x % 2 != 0) && (x % 3 != 0) && (x % 5 != 0) && (x % 7 != 0) && 45 | (x % 11 != 0) && (x % 13 != 0) && (x % 17 != 0); 46 | 47 | int count = numEntries; 48 | while (!IsMoreOrLessPrime(count)) 49 | { 50 | count++; 51 | } 52 | 53 | return count; 54 | } 55 | 56 | /// 57 | /// Calculates the hash for an entry. 58 | /// 59 | /// The seed value. 60 | /// The name of the entry. 61 | /// The calculated hash. 62 | public static uint CalculateHash(uint seed, byte[] name) 63 | { 64 | uint hash = seed ^ 123456789; 65 | for (int i = 0; i < name.Length; i += 2) { 66 | hash = (hash >> 5) | (hash << 27); 67 | hash ^= (ushort)(name[i] | (name[i + 1] << 8)); 68 | } 69 | 70 | return hash; 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /src/Lemon.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.8.34330.188 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lemon", "Lemon\Lemon.csproj", "{BDA81BC9-1077-4348-BA57-3189FC735957}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lemon.IntegrationTests", "Lemon.IntegrationTests\Lemon.IntegrationTests.csproj", "{59AC2D93-C7DD-4341-9FB8-EBC7AD8BB221}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F0B0CB72-632C-4975-9C83-28102ABA73FE}" 11 | ProjectSection(SolutionItems) = preProject 12 | Directory.Build.props = Directory.Build.props 13 | Directory.Packages.props = Directory.Packages.props 14 | nuget.config = nuget.config 15 | Tests.runsettings = Tests.runsettings 16 | EndProjectSection 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Debug|x64 = Debug|x64 22 | Debug|x86 = Debug|x86 23 | Release|Any CPU = Release|Any CPU 24 | Release|x64 = Release|x64 25 | Release|x86 = Release|x86 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {BDA81BC9-1077-4348-BA57-3189FC735957}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {BDA81BC9-1077-4348-BA57-3189FC735957}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {BDA81BC9-1077-4348-BA57-3189FC735957}.Debug|x64.ActiveCfg = Debug|Any CPU 31 | {BDA81BC9-1077-4348-BA57-3189FC735957}.Debug|x64.Build.0 = Debug|Any CPU 32 | {BDA81BC9-1077-4348-BA57-3189FC735957}.Debug|x86.ActiveCfg = Debug|Any CPU 33 | {BDA81BC9-1077-4348-BA57-3189FC735957}.Debug|x86.Build.0 = Debug|Any CPU 34 | {BDA81BC9-1077-4348-BA57-3189FC735957}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {BDA81BC9-1077-4348-BA57-3189FC735957}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {BDA81BC9-1077-4348-BA57-3189FC735957}.Release|x64.ActiveCfg = Release|Any CPU 37 | {BDA81BC9-1077-4348-BA57-3189FC735957}.Release|x64.Build.0 = Release|Any CPU 38 | {BDA81BC9-1077-4348-BA57-3189FC735957}.Release|x86.ActiveCfg = Release|Any CPU 39 | {BDA81BC9-1077-4348-BA57-3189FC735957}.Release|x86.Build.0 = Release|Any CPU 40 | {59AC2D93-C7DD-4341-9FB8-EBC7AD8BB221}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {59AC2D93-C7DD-4341-9FB8-EBC7AD8BB221}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {59AC2D93-C7DD-4341-9FB8-EBC7AD8BB221}.Debug|x64.ActiveCfg = Debug|Any CPU 43 | {59AC2D93-C7DD-4341-9FB8-EBC7AD8BB221}.Debug|x64.Build.0 = Debug|Any CPU 44 | {59AC2D93-C7DD-4341-9FB8-EBC7AD8BB221}.Debug|x86.ActiveCfg = Debug|Any CPU 45 | {59AC2D93-C7DD-4341-9FB8-EBC7AD8BB221}.Debug|x86.Build.0 = Debug|Any CPU 46 | {59AC2D93-C7DD-4341-9FB8-EBC7AD8BB221}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {59AC2D93-C7DD-4341-9FB8-EBC7AD8BB221}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {59AC2D93-C7DD-4341-9FB8-EBC7AD8BB221}.Release|x64.ActiveCfg = Release|Any CPU 49 | {59AC2D93-C7DD-4341-9FB8-EBC7AD8BB221}.Release|x64.Build.0 = Release|Any CPU 50 | {59AC2D93-C7DD-4341-9FB8-EBC7AD8BB221}.Release|x86.ActiveCfg = Release|Any CPU 51 | {59AC2D93-C7DD-4341-9FB8-EBC7AD8BB221}.Release|x86.Build.0 = Release|Any CPU 52 | EndGlobalSection 53 | GlobalSection(SolutionProperties) = preSolution 54 | HideSolutionNode = FALSE 55 | EndGlobalSection 56 | EndGlobal 57 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Containers/TestData.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.IntegrationTests.Containers 21 | { 22 | using System.Collections; 23 | using System.IO; 24 | using System.Linq; 25 | using NUnit.Framework; 26 | 27 | public static class TestData 28 | { 29 | public static IEnumerable NcsdParams { 30 | get => GetStreamAndInfoCollection("ncsd.txt"); 31 | } 32 | 33 | public static IEnumerable CiaParams { 34 | get => GetStreamAndInfoCollection("cia.txt"); 35 | } 36 | 37 | public static IEnumerable NcchParams { 38 | get => GetSubstreamAndInfoCollection("ncch.txt"); 39 | } 40 | 41 | public static IEnumerable ExeFsParams { 42 | get => GetSubstreamAndInfoCollection("exefs.txt"); 43 | } 44 | 45 | public static IEnumerable IvfcParams { 46 | get => GetSubstreamAndInfoCollection("ivfc.txt"); 47 | } 48 | 49 | public static string ContainersResources { 50 | get => Path.Combine(TestDataBase.RootFromOutputPath, "containers"); 51 | } 52 | 53 | private static IEnumerable GetStreamAndInfoCollection(string listName) 54 | { 55 | return TestDataBase.ReadTestListFile(Path.Combine(ContainersResources, listName)) 56 | .Select(line => line.Split(',')) 57 | .Select(data => new TestFixtureData( 58 | Path.Combine(ContainersResources, data[0]), 59 | Path.Combine(ContainersResources, data[1])) 60 | .SetArgDisplayNames(data[0], data[1])); 61 | } 62 | 63 | private static IEnumerable GetSubstreamAndInfoCollection(string listName) 64 | { 65 | return TestDataBase.ReadTestListFile(Path.Combine(ContainersResources, listName)) 66 | .Select(line => line.Split(',')) 67 | .Select(data => new TestFixtureData( 68 | Path.Combine(ContainersResources, data[0]), 69 | Path.Combine(ContainersResources, data[1]), 70 | int.Parse(data[2]), 71 | int.Parse(data[3])) 72 | .SetArgDisplayNames(data[0], data[1])); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of 9 | experience, nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or reject 41 | comments, commits, code, wiki edits, issues, and other contributions that are 42 | not aligned to this Code of Conduct, or to ban temporarily or permanently any 43 | contributor for other behaviors that they deem inappropriate, threatening, 44 | offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at . The project 59 | team will review and investigate all complaints, and will respond in a way that 60 | it deems appropriate to the circumstances. The project team is obligated to 61 | maintain confidentiality with regard to the reporter of an incident. Further 62 | details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 71 | version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /src/Lemon/Containers/Formats/NcchHeader.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.Containers.Formats 21 | { 22 | using System.Diagnostics.CodeAnalysis; 23 | 24 | /// 25 | /// NCCH header information. 26 | /// 27 | public class NcchHeader 28 | { 29 | /// 30 | /// Gets the magic identifier of the format. 31 | /// 32 | /// The magic ID of the format. 33 | public static string MagicId { 34 | get { return "NCCH"; } 35 | } 36 | 37 | /// 38 | /// Gets the equivalent in bytes of one unit for the header values. 39 | /// 40 | /// One unit in bytes. 41 | public static int Unit { 42 | get { return 0x200; } 43 | } 44 | 45 | /// 46 | /// Gets or sets the RSA-2048 signature of the NCCH header using SHA-256. 47 | /// 48 | /// The signature of the header. 49 | [SuppressMessage( 50 | "Microsoft.Performance", 51 | "CA1819:PropertiesShouldNotReturnArrays", 52 | Justification="Model or DTO.")] 53 | public byte[] Signature { 54 | get; 55 | set; 56 | } 57 | 58 | /// 59 | /// Gets or sets the partition id. 60 | /// 61 | public long PartitionId { get; set; } 62 | 63 | /// 64 | /// Gets or sets the maker code. 65 | /// 66 | public short MakerCode { get; set; } 67 | 68 | /// 69 | /// Gets or sets the version. 70 | /// 71 | public short Version { get; set; } 72 | 73 | /// 74 | /// Gets or sets the program id. 75 | /// 76 | public long ProgramId { get; set; } 77 | 78 | /// 79 | /// Gets or sets the product code. 80 | /// 81 | public string ProductCode { get; set; } 82 | 83 | /// 84 | /// Gets or sets the flags. 85 | /// 86 | public byte[] Flags { get; set; } 87 | 88 | /// 89 | /// Gets or sets the size of the system region (in units) that will be used for calculating the hash. 90 | /// 91 | public int SystemHashSize { get; set; } 92 | 93 | /// 94 | /// Gets or sets the size of the program region (in units) that will be used for calculating the hash. 95 | /// 96 | public int RomHashSize { get; set; } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Titles/Binary2TitleMetadataTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.IntegrationTests.Titles 21 | { 22 | using System.IO; 23 | using System.Text.RegularExpressions; 24 | using FluentAssertions; 25 | using FluentAssertions.Equivalency; 26 | using NUnit.Framework; 27 | using SceneGate.Lemon.Titles; 28 | using YamlDotNet.Serialization; 29 | using YamlDotNet.Serialization.NamingConventions; 30 | using Yarhl.FileFormat; 31 | using Yarhl.IO; 32 | 33 | [TestFixtureSource(typeof(TestData), nameof(TestData.TmdParams))] 34 | public class Binary2TitleMetadataTests : Binary2ObjectTests 35 | { 36 | readonly string yamlPath; 37 | readonly string binaryPath; 38 | readonly int offset; 39 | readonly int size; 40 | 41 | public Binary2TitleMetadataTests(string yamlPath, string binaryPath, int offset, int size) 42 | { 43 | this.yamlPath = yamlPath; 44 | this.binaryPath = binaryPath; 45 | this.offset = offset; 46 | this.size = size; 47 | 48 | TestDataBase.IgnoreIfFileDoesNotExist(binaryPath); 49 | TestDataBase.IgnoreIfFileDoesNotExist(yamlPath); 50 | } 51 | 52 | protected override void AssertObjects(TitleMetadata actual, TitleMetadata expected) 53 | { 54 | actual.Should().BeEquivalentTo( 55 | expected, 56 | opts => opts.Excluding(t => t.Signature) 57 | .Excluding(t => t.Hash) 58 | .Excluding((IMemberInfo i) => i.RemovingIndexes() == "InfoRecords.Hash") 59 | .Excluding((IMemberInfo i) => i.RemovingIndexes() == "Chunks.Hash")); 60 | } 61 | 62 | protected override BinaryFormat GetBinary() 63 | { 64 | TestContext.WriteLine($"{nameof(Binary2TitleMetadataTests)}: {Path.GetFileName(binaryPath)}"); 65 | var stream = DataStreamFactory.FromFile(binaryPath, FileOpenMode.Read, offset, size); 66 | return new BinaryFormat(stream); 67 | } 68 | 69 | protected override TitleMetadata GetObject() 70 | { 71 | string yaml = File.ReadAllText(yamlPath); 72 | return new DeserializerBuilder() 73 | .WithNamingConvention(UnderscoredNamingConvention.Instance) 74 | .Build() 75 | .Deserialize(yaml); 76 | } 77 | 78 | protected override IConverter GetToObjectConverter() 79 | { 80 | return new Binary2TitleMetadata(); 81 | } 82 | 83 | protected override IConverter GetToBinaryConverter() 84 | { 85 | return new TitleMetadata2Binary(); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Titles/Binary2ObjectTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.IntegrationTests.Titles 21 | { 22 | using FluentAssertions; 23 | using NUnit.Framework; 24 | using Yarhl.FileFormat; 25 | using Yarhl.IO; 26 | 27 | public abstract class Binary2ObjectTests 28 | where T : IFormat 29 | { 30 | int initialStreams; 31 | BinaryFormat original; 32 | T expected; 33 | IConverter objectConverter; 34 | IConverter binaryConverter; 35 | 36 | [OneTimeSetUp] 37 | public void SetUpFixture() 38 | { 39 | expected = GetObject(); 40 | objectConverter = GetToObjectConverter(); 41 | binaryConverter = GetToBinaryConverter(); 42 | } 43 | 44 | [TearDown] 45 | public void TearDown() 46 | { 47 | original?.Dispose(); 48 | 49 | // Make sure we didn't leave anything without dispose. 50 | Assert.That(DataStream.ActiveStreams, Is.EqualTo(initialStreams)); 51 | } 52 | 53 | [SetUp] 54 | public void SetUp() 55 | { 56 | // By opening and disposing in each we prevent other tests failing 57 | // because the file is still open. 58 | initialStreams = DataStream.ActiveStreams; 59 | original = GetBinary(); 60 | } 61 | 62 | [Test] 63 | public void TransformToObject() 64 | { 65 | int numStreams = DataStream.ActiveStreams; 66 | 67 | T actual = objectConverter.Convert(original); 68 | AssertObjects(actual, expected); 69 | 70 | // Check everything is virtual node 71 | Assert.That(DataStream.ActiveStreams, Is.EqualTo(numStreams)); 72 | } 73 | 74 | [Test] 75 | public void TransformBothWays() 76 | { 77 | if (binaryConverter == null) { 78 | Assert.Ignore(); 79 | } 80 | 81 | T actual = objectConverter.Convert(original); 82 | using var actualBinary = binaryConverter.Convert(actual); 83 | 84 | Assert.That(original.Stream.Compare(actualBinary.Stream), Is.True, "Binaries are not identical"); 85 | } 86 | 87 | protected virtual void AssertObjects(T actual, T expected) 88 | { 89 | actual.Should().BeEquivalentTo(expected); 90 | } 91 | 92 | protected abstract BinaryFormat GetBinary(); 93 | 94 | protected abstract T GetObject(); 95 | 96 | protected abstract IConverter GetToObjectConverter(); 97 | 98 | protected abstract IConverter GetToBinaryConverter(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Lemon/Containers/Formats/NcsdHeader.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.Containers.Formats 21 | { 22 | using System.Diagnostics.CodeAnalysis; 23 | 24 | /// 25 | /// NCCH header information. 26 | /// 27 | public class NcsdHeader 28 | { 29 | /// 30 | /// Gets the magic identifier of the format. 31 | /// 32 | /// The magic ID of the format. 33 | public static string MagicId { 34 | get { return "NCSD"; } 35 | } 36 | 37 | /// 38 | /// Gets the equivalent in bytes of one unit for the header values. 39 | /// 40 | /// One unit in bytes. 41 | public static int Unit { 42 | get { return 0x200; } 43 | } 44 | 45 | /// 46 | /// Gets or sets the RSA-2048 signature of the NCCH header using SHA-256. 47 | /// 48 | /// The signature of the header. 49 | [SuppressMessage( 50 | "Microsoft.Performance", 51 | "CA1819:PropertiesShouldNotReturnArrays", 52 | Justification="Model or DTO.")] 53 | public byte[] Signature { 54 | get; 55 | set; 56 | } 57 | 58 | /// 59 | /// Gets or sets the size of the format. 60 | /// 61 | /// The size of the format in bytes. 62 | public long Size { 63 | get; 64 | set; 65 | } 66 | 67 | /// 68 | /// Gets or sets the media ID. 69 | /// 70 | /// The media ID. 71 | public ulong MediaId { 72 | get; 73 | set; 74 | } 75 | 76 | /// 77 | /// Gets or sets the type of the firmware for each partition. 78 | /// 79 | /// The type of the partitions firmware. 80 | [SuppressMessage( 81 | "Microsoft.Performance", 82 | "CA1819:PropertiesShouldNotReturnArrays", 83 | Justification="Model or DTO.")] 84 | public FirmwareType[] FirmwaresType { 85 | get; 86 | set; 87 | } 88 | 89 | /// 90 | /// Gets or sets the encryption type of each partition. 91 | /// 92 | /// The encryption type of each partition. 93 | [SuppressMessage( 94 | "Microsoft.Performance", 95 | "CA1819:PropertiesShouldNotReturnArrays", 96 | Justification="Model or DTO.")] 97 | public byte[] CryptType { 98 | get; 99 | set; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lemon 2 | 3 | 4 |

5 | 6 | Stable version 7 | 8 |   9 | 10 | GitHub commits since latest release (by SemVer) 11 | 12 |   13 | 14 | Build and release 15 | 16 |   17 | 18 | MIT License 19 | 20 |   21 |

22 | 23 | _Lemon_ is a library part of the [_SceneGate_](https://github.com/SceneGate) 24 | framework that provides support for **3DS file formats.** 25 | 26 | ## Supported formats 27 | 28 | _Encryption, decryption or signature validation not supported yet._ 29 | 30 | - **NCSD (CCI and CSU)**: unpack 31 | - **CIA**: unpack and pack 32 | - **NCCH (CXI and CFA)**: unpack and pack 33 | - **ExeFS**: unpack and pack 34 | - **RomFS**: unpack and pack 35 | - **TMD**: deserialize and serialize 36 | 37 | ## Usage 38 | 39 | The project provides the following .NET libraries (NuGet packages in nuget.org). 40 | The libraries works on supported versions of .NET: 6.0 and 8.0. 41 | 42 | - [![SceneGate.Lemon](https://img.shields.io/nuget/v/SceneGate.Lemon?label=SceneGate.Lemon&logo=nuget)](https://www.nuget.org/packages/SceneGate.Lemon): 43 | support for 3DS formats 44 | 45 | Preview releases can be found in this 46 | [Azure DevOps package repository](https://dev.azure.com/SceneGate/SceneGate/_packaging?_a=feed&feed=SceneGate-Preview). 47 | To use a preview release, create a file `nuget.config` in the same directory of 48 | your solution file (.sln) with the following content: 49 | 50 | ```xml 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | ``` 70 | 71 | ## Documentation 72 | 73 | Documentation is not yet available but it will be published in the 74 | [project website](https://scenegate.github.io/Lemon). 75 | 76 | Don't hesitate to ask questions in the 77 | [project Discussion site!](https://github.com/SceneGate/Ekona/discussions) 78 | 79 | ## Build 80 | 81 | The project requires to build .NET 8.0 SDK. 82 | 83 | To build, test and generate artifacts run: 84 | 85 | ```sh 86 | # Build and run tests 87 | dotnet run --project build/orchestrator 88 | 89 | # (Optional) Create bundles (nuget, zips, docs) 90 | dotnet run --project build/orchestrator -- --target=Bundle 91 | ``` 92 | 93 | Some test binary resources are pushed via [Git LFS](https://git-lfs.com/). Make 94 | sure to clone these files as well, otherwise the tests would fail. On Linux you 95 | may need to install it and re-pull, for instance for Ubuntu run: 96 | 97 | ```sh 98 | sudo apt install git-lfs 99 | git lfs pull 100 | ``` 101 | 102 | To build the documentation only, run: 103 | 104 | ```sh 105 | dotnet docfx docs/docfx.json --serve 106 | ``` 107 | 108 | ## References 109 | 110 | - [3D Brew](https://www.3dbrew.org/wiki/Main_Page) 111 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Containers/NcsdConverterTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.IntegrationTests.Containers 21 | { 22 | using System.IO; 23 | using NUnit.Framework; 24 | using SceneGate.Lemon.Containers.Converters; 25 | using SceneGate.Lemon.Containers.Formats; 26 | using YamlDotNet.Serialization; 27 | using YamlDotNet.Serialization.NamingConventions; 28 | using Yarhl.FileSystem; 29 | using Yarhl.IO; 30 | 31 | [TestFixtureSource(typeof(TestData), nameof(TestData.NcsdParams))] 32 | public class NcsdConverterTests 33 | { 34 | readonly string ncsdPath; 35 | readonly string yamlPath; 36 | 37 | Node actualNode; 38 | Ncsd actual; 39 | NcsdTestInfo expected; 40 | 41 | public NcsdConverterTests(string ncsdPath, string yamlPath) 42 | { 43 | this.ncsdPath = ncsdPath; 44 | this.yamlPath = yamlPath; 45 | } 46 | 47 | [OneTimeSetUp] 48 | public void SetUpFixture() 49 | { 50 | TestDataBase.IgnoreIfFileDoesNotExist(ncsdPath); 51 | TestDataBase.IgnoreIfFileDoesNotExist(yamlPath); 52 | 53 | actualNode = NodeFactory.FromFile(ncsdPath); 54 | Assert.That(() => actualNode.TransformWith(), Throws.Nothing); 55 | actual = actualNode.GetFormatAs(); 56 | 57 | string yaml = File.ReadAllText(yamlPath); 58 | expected = new DeserializerBuilder() 59 | .WithNamingConvention(UnderscoredNamingConvention.Instance) 60 | .Build() 61 | .Deserialize(yaml); 62 | } 63 | 64 | [OneTimeTearDown] 65 | public void TearDownFixture() 66 | { 67 | actualNode?.Dispose(); 68 | Assert.That(DataStream.ActiveStreams, Is.EqualTo(0)); 69 | } 70 | 71 | [Test] 72 | public void ValidateHeader() 73 | { 74 | var header = actual.Header; 75 | Assert.That(header.Signature, Has.Length.EqualTo(expected.SignatureLength)); 76 | Assert.That(header.Size, Is.EqualTo(expected.Size)); 77 | Assert.That(header.MediaId, Is.EqualTo(expected.MediaId)); 78 | Assert.That(header.FirmwaresType, Is.EquivalentTo(expected.FirmwaresType)); 79 | Assert.That(header.CryptType, Is.EquivalentTo(expected.CryptType)); 80 | } 81 | 82 | [Test] 83 | public void ValidatePartitions() 84 | { 85 | Assert.That( 86 | actual.Root.Children, 87 | Has.Count.EqualTo(expected.AvailablePartitions.Length)); 88 | 89 | for (int i = 0; i < actual.Root.Children.Count; i++) { 90 | var child = actual.Root.Children[i]; 91 | Assert.That(child.Name, Is.EqualTo(expected.AvailablePartitions[i])); 92 | Assert.That(child.Stream.Offset, Is.EqualTo(expected.PartitionsOffset[i])); 93 | Assert.That(child.Stream.Length, Is.EqualTo(expected.PartitionsSize[i])); 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Lemon/Containers/Converters/Binary2Ncsd.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.Containers.Converters 21 | { 22 | using System; 23 | using System.Diagnostics.CodeAnalysis; 24 | using System.Linq; 25 | using SceneGate.Lemon.Containers.Formats; 26 | using Yarhl.FileFormat; 27 | using Yarhl.FileSystem; 28 | using Yarhl.IO; 29 | 30 | /// 31 | /// Converter for Binary streams into a NCSD instance. 32 | /// 33 | public class Binary2Ncsd : IConverter 34 | { 35 | /// 36 | /// Gets the name of a partition from its index. 37 | /// 38 | /// Index of the partition. 39 | /// The associated partition's name. 40 | public static string GetPartitionName(int index) 41 | { 42 | switch (index) { 43 | case 0: 44 | return "program"; 45 | case 1: 46 | return "manual"; 47 | case 2: 48 | return "download_play"; 49 | case 6: 50 | return "new3ds_update"; 51 | case 7: 52 | return "update"; 53 | default: 54 | throw new FormatException("Unsupported partition"); 55 | } 56 | } 57 | 58 | /// 59 | /// Converts a binary stream into a NCSD instance. 60 | /// 61 | /// Binary stream to convert. 62 | /// The new NCSD instance. 63 | [SuppressMessage("Reliability", "CA2000", Justification = "Transfer ownership")] 64 | public Ncsd Convert(BinaryFormat source) 65 | { 66 | if (source == null) 67 | throw new ArgumentNullException(nameof(source)); 68 | 69 | var ncsd = new Ncsd(); 70 | var reader = new DataReader(source.Stream); 71 | 72 | // First read the header 73 | var header = ncsd.Header; 74 | header.Signature = reader.ReadBytes(0x100); 75 | if (reader.ReadString(4) != NcsdHeader.MagicId) 76 | throw new FormatException("Invalid Magic ID"); 77 | 78 | header.Size = reader.ReadUInt32() * NcsdHeader.Unit; 79 | header.MediaId = reader.ReadUInt64(); 80 | header.FirmwaresType = reader.ReadBytes(Ncsd.NumPartitions) 81 | .Select(x => (FirmwareType)x) 82 | .ToArray(); 83 | header.CryptType = reader.ReadBytes(Ncsd.NumPartitions); 84 | 85 | // Now add the subfiles / partitions 86 | for (int i = 0; i < Ncsd.NumPartitions; i++) { 87 | long offset = reader.ReadUInt32() * NcsdHeader.Unit; 88 | long size = reader.ReadUInt32() * NcsdHeader.Unit; 89 | if (size == 0) { 90 | continue; 91 | } 92 | 93 | string name = (header.FirmwaresType[i] == FirmwareType.None) 94 | ? GetPartitionName(i) 95 | : $"firm{i}"; 96 | var childBinary = new BinaryFormat(source.Stream, offset, size); 97 | var child = new Node(name, childBinary); 98 | ncsd.Root.Add(child); 99 | } 100 | 101 | // TODO: Read rest of header 102 | return ncsd; 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Lemon/Titles/TitleMetadata.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.Titles 21 | { 22 | using System.Collections.ObjectModel; 23 | using Yarhl.FileFormat; 24 | 25 | /// 26 | /// E-shop title metadata. 27 | /// 28 | public class TitleMetadata : IFormat 29 | { 30 | /// 31 | /// Initializes a new instance of the class. 32 | /// 33 | public TitleMetadata() 34 | { 35 | InfoRecords = new Collection(); 36 | Chunks = new Collection(); 37 | } 38 | 39 | /// 40 | /// Gets or sets the signature type. 41 | /// 42 | public uint SignType { get; set; } 43 | 44 | /// 45 | /// Gets or sets the signature. 46 | /// 47 | public byte[] Signature { get; set; } 48 | 49 | /// 50 | /// Gets or sets a value indicating whether the title metadata has a 51 | /// valid signature. 52 | /// 53 | public bool ValidSignature { get; set; } 54 | 55 | /// 56 | /// Gets or sets the signature issuer. 57 | /// 58 | public string SignatureIssuer { get; set; } 59 | 60 | /// 61 | /// Gets or sets the version of the title metadata. 62 | /// 63 | public byte Version { get; set; } 64 | 65 | /// 66 | /// Gets or sets the version of the CA CRL. 67 | /// 68 | public byte CaCrlVersion { get; set; } 69 | 70 | /// 71 | /// Gets or sets the version of the signer CRL. 72 | /// 73 | public byte SignerCrlVersion { get; set; } 74 | 75 | /// 76 | /// Gets or sets the system's version. 77 | /// 78 | public long SystemVersion { get; set; } 79 | 80 | /// 81 | /// Gets or sets the ID of the title. 82 | /// 83 | public long TitleId { get; set; } 84 | 85 | /// 86 | /// Gets or sets the type of title. 87 | /// 88 | public int TitleType { get; set; } 89 | 90 | /// 91 | /// Gets or sets the ID of the group. 92 | /// 93 | public short GroupId { get; set; } 94 | 95 | /// 96 | /// Gets or sets the size of the save file data. 97 | /// 98 | public int SaveSize { get; set; } 99 | 100 | /// 101 | /// Gets or sets the size of the private data of a SRL (DSi) save file. 102 | /// 103 | public int SrlPrivateSaveSize { get; set; } 104 | 105 | /// 106 | /// Gets or sets the SRL (DSi) flags. 107 | /// 108 | public byte SrlFlag { get; set; } 109 | 110 | /// 111 | /// Gets or sets the access rights. 112 | /// 113 | public int AccessRights { get; set; } 114 | 115 | /// 116 | /// Gets or sets the version of the title. 117 | /// 118 | public short TitleVersion { get; set; } 119 | 120 | /// 121 | /// Gets or sets the index of the bootable content. 122 | /// 123 | public int BootContent { get; set; } 124 | 125 | /// 126 | /// Gets or sets the hash of the Content Info Records. 127 | /// 128 | public byte[] Hash { get; set; } 129 | 130 | /// 131 | /// Gets a collection of content info records. 132 | /// 133 | public Collection InfoRecords { get; private set; } 134 | 135 | /// 136 | /// Gets a collection of content chunks. 137 | /// 138 | public Collection Chunks { get; private set; } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Containers/Binary2ContainerTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.IntegrationTests.Containers 21 | { 22 | using NUnit.Framework; 23 | using Yarhl.FileFormat; 24 | using Yarhl.FileSystem; 25 | using Yarhl.IO; 26 | 27 | public abstract class Binary2ContainerTests 28 | { 29 | private int initialStreams; 30 | private BinaryFormat original; 31 | private NodeContainerInfo containerInfo; 32 | private IConverter containerConverter; 33 | private IConverter binaryConverter; 34 | 35 | [OneTimeSetUp] 36 | public void SetUpFixture() 37 | { 38 | containerInfo = GetContainerInfo(); 39 | containerConverter = GetToContainerConverter(); 40 | binaryConverter = GetToBinaryConverter(); 41 | } 42 | 43 | [TearDown] 44 | public void TearDown() 45 | { 46 | original?.Dispose(); 47 | 48 | // Make sure we didn't leave anything without dispose. 49 | Assert.That(DataStream.ActiveStreams, Is.EqualTo(initialStreams)); 50 | } 51 | 52 | [SetUp] 53 | public void SetUp() 54 | { 55 | // By opening and disposing in each we prevent other tests failing 56 | // because the file is still open. 57 | initialStreams = DataStream.ActiveStreams; 58 | original = GetBinary(); 59 | } 60 | 61 | [Test] 62 | public void TransformToContainer() 63 | { 64 | // Check nodes are expected 65 | using var nodes = containerConverter.Convert(original); 66 | CheckNode(containerInfo, nodes.Root); 67 | 68 | // Check everything is virtual node (only the binary stream) 69 | Assert.That(DataStream.ActiveStreams, Is.EqualTo(initialStreams + 1)); 70 | } 71 | 72 | [Test] 73 | public void TransformBothWays() 74 | { 75 | if (binaryConverter == null) { 76 | Assert.Ignore(); 77 | } 78 | 79 | using var nodes = containerConverter.Convert(original); 80 | using var actualBinary = binaryConverter.Convert(nodes); 81 | 82 | Assert.That(original.Stream.Compare(actualBinary.Stream), Is.True, "Streams are not identical"); 83 | } 84 | 85 | protected abstract BinaryFormat GetBinary(); 86 | 87 | protected abstract NodeContainerInfo GetContainerInfo(); 88 | 89 | protected abstract IConverter GetToContainerConverter(); 90 | 91 | protected abstract IConverter GetToBinaryConverter(); 92 | 93 | private static void CheckNode(NodeContainerInfo info, Node node) 94 | { 95 | Assert.That(node.Name, Is.EqualTo(info.Name), node.Path); 96 | Assert.That(node.Format?.GetType().FullName, Is.EqualTo(info.FormatType), node.Path); 97 | 98 | if (info.Tags != null) { 99 | // YAML deserializer always gets the value as a string 100 | foreach (var entry in info.Tags) { 101 | Assert.That(node.Tags.ContainsKey(entry.Key), Is.True, node.Path); 102 | Assert.That(node.Tags[entry.Key].ToString(), Is.EqualTo(entry.Value), node.Path); 103 | } 104 | } 105 | 106 | if (info.StreamLength > 0) { 107 | Assert.That(node.Stream.Offset, Is.EqualTo(info.StreamOffset), $"Invalid offset for: {node.Path}"); 108 | Assert.That(node.Stream.Length, Is.EqualTo(info.StreamLength), $"Invalid length for: {node.Path}"); 109 | } 110 | 111 | if (info.CheckChildren) { 112 | int expectedCount = info.Children?.Count ?? 0; 113 | Assert.That(expectedCount, Is.EqualTo(node.Children.Count), node.Path); 114 | 115 | for (int i = 0; i < expectedCount; i++) { 116 | CheckNode(info.Children[i], node.Children[i]); 117 | } 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Lemon/Containers/Converters/Binary2Ncch.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.Containers.Converters 21 | { 22 | using System; 23 | using System.Diagnostics.CodeAnalysis; 24 | using SceneGate.Lemon.Containers.Formats; 25 | using Yarhl.FileFormat; 26 | using Yarhl.FileSystem; 27 | using Yarhl.IO; 28 | 29 | /// 30 | /// Converter for Binary streams into a NCCH instance. 31 | /// 32 | public class Binary2Ncch : IConverter 33 | { 34 | const int HeaderLength = 0x200; 35 | const int AccessDescriptorLength = 0x400; 36 | 37 | /// 38 | /// Converts a binary stream into a NCCH instance. 39 | /// 40 | /// Binary stream to convert. 41 | /// The new NCCH instance. 42 | public Ncch Convert(BinaryFormat source) 43 | { 44 | if (source == null) 45 | throw new ArgumentNullException(nameof(source)); 46 | 47 | var ncch = new Ncch(); 48 | var reader = new DataReader(source.Stream); 49 | reader.Stream.Position = 0; 50 | 51 | // First read the header 52 | var header = ncch.Header; 53 | header.Signature = reader.ReadBytes(0x100); 54 | if (reader.ReadString(4) != NcchHeader.MagicId) 55 | throw new FormatException("Invalid Magic ID"); 56 | 57 | source.Stream.Position = 0x108; 58 | header.PartitionId = reader.ReadInt64(); 59 | header.MakerCode = reader.ReadInt16(); 60 | header.Version = reader.ReadInt16(); 61 | 62 | source.Stream.Position = 0x118; 63 | header.ProgramId = reader.ReadInt64(); 64 | 65 | source.Stream.Position = 0x150; 66 | header.ProductCode = reader.ReadString(0x10).Replace("\0", string.Empty); 67 | 68 | source.Stream.Position = 0x188; 69 | byte[] flags = new byte[0x8]; 70 | for (int i = 0; i < flags.Length; i++) { 71 | flags[i] = reader.ReadByte(); 72 | } 73 | 74 | header.Flags = flags; 75 | 76 | source.Stream.Position = 0x180; 77 | uint exHeaderLength = reader.ReadUInt32(); 78 | if (exHeaderLength != 0) { 79 | AddExtendedHeader(ncch.Root, exHeaderLength, source.Stream); 80 | } 81 | 82 | // Read the subfiles 83 | source.Stream.Position = 0x190; 84 | AddChildIfExists("sdk_info.txt", ncch.Root, reader); 85 | 86 | AddChildIfExists("logo.bin", ncch.Root, reader); 87 | 88 | AddChildIfExists("system", ncch.Root, reader); 89 | 90 | header.SystemHashSize = reader.ReadInt32(); 91 | source.Stream.Position += 4; // Reserved 92 | 93 | AddChildIfExists("rom", ncch.Root, reader); 94 | 95 | header.RomHashSize = reader.ReadInt32(); 96 | 97 | return ncch; 98 | } 99 | 100 | static void AddExtendedHeader(Node root, uint length, DataStream baseStream) 101 | { 102 | // Extended header is just after the NCCH header 103 | var extendedHeader = NodeFactory.FromSubstream( 104 | "extended_header", 105 | baseStream, 106 | HeaderLength, 107 | length); 108 | root.Add(extendedHeader); 109 | 110 | // Access descriptor is just after the extended header 111 | var accessDescriptor = NodeFactory.FromSubstream( 112 | "access_descriptor", 113 | baseStream, 114 | HeaderLength + length, 115 | AccessDescriptorLength); 116 | root.Add(accessDescriptor); 117 | } 118 | 119 | [SuppressMessage("Reliability", "CA2000", Justification = "Transfer ownership")] 120 | static void AddChildIfExists(string name, Node root, DataReader reader) 121 | { 122 | BinaryFormat binary = ReadBinaryChild(reader); 123 | if (binary != null) { 124 | root.Add(new Node(name, binary)); 125 | } 126 | } 127 | 128 | static BinaryFormat ReadBinaryChild(DataReader reader) 129 | { 130 | long offset = reader.ReadUInt32() * NcchHeader.Unit; 131 | long size = reader.ReadUInt32() * NcchHeader.Unit; 132 | if (size == 0) { 133 | return null; 134 | } 135 | 136 | return new BinaryFormat(reader.Stream, offset, size); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Lemon.IntegrationTests/Containers/NcchConverterTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.IntegrationTests.Containers 21 | { 22 | using System.IO; 23 | using NUnit.Framework; 24 | using SceneGate.Lemon.Containers.Converters; 25 | using SceneGate.Lemon.Containers.Formats; 26 | using YamlDotNet.Serialization; 27 | using YamlDotNet.Serialization.NamingConventions; 28 | using Yarhl.IO; 29 | 30 | [TestFixtureSource(typeof(TestData), nameof(TestData.NcchParams))] 31 | public class NcchConverterTests 32 | { 33 | private readonly string yamlPath; 34 | private readonly string binaryPath; 35 | private readonly int offset; 36 | private readonly int size; 37 | 38 | private int initialStreams; 39 | private BinaryFormat original; 40 | private Binary2Ncch containerConverter; 41 | private Ncch2Binary binaryConverter; 42 | 43 | private NcchTestInfo expected; 44 | 45 | public NcchConverterTests(string yamlPath, string binaryPath, int offset, int size) 46 | { 47 | this.yamlPath = yamlPath; 48 | this.binaryPath = binaryPath; 49 | this.offset = offset; 50 | this.size = size; 51 | 52 | TestDataBase.IgnoreIfFileDoesNotExist(binaryPath); 53 | TestDataBase.IgnoreIfFileDoesNotExist(yamlPath); 54 | } 55 | 56 | [OneTimeSetUp] 57 | public void SetUpFixture() 58 | { 59 | containerConverter = new Binary2Ncch(); 60 | binaryConverter = new Ncch2Binary(); 61 | 62 | string yaml = File.ReadAllText(yamlPath); 63 | expected = new DeserializerBuilder() 64 | .WithNamingConvention(UnderscoredNamingConvention.Instance) 65 | .Build() 66 | .Deserialize(yaml); 67 | } 68 | 69 | [TearDown] 70 | public void TearDown() 71 | { 72 | original?.Dispose(); 73 | 74 | // Make sure we didn't leave anything without dispose. 75 | Assert.That(DataStream.ActiveStreams, Is.EqualTo(initialStreams)); 76 | } 77 | 78 | [SetUp] 79 | public void SetUp() 80 | { 81 | // By opening and disposing in each we prevent other tests failing 82 | // because the file is still open. 83 | initialStreams = DataStream.ActiveStreams; 84 | original = GetBinary(offset, size); 85 | } 86 | 87 | [Test] 88 | public void TransformThreeWays() 89 | { 90 | if (binaryConverter == null) { 91 | Assert.Ignore(); 92 | } 93 | 94 | // Convert the binary to NCCH, and check the original header and regions are expected 95 | using var actual = containerConverter.Convert(original); 96 | ValidateHeader(actual); 97 | ValidateRegions(actual, true); 98 | 99 | // Convert the new NCCH to binary (and vice-versa), and check the header and regions lengths are expected 100 | using var generatedBinary = binaryConverter.Convert(actual); 101 | using var generatedNcch = containerConverter.Convert(generatedBinary); 102 | ValidateHeader(generatedNcch); 103 | ValidateRegions(generatedNcch, false); 104 | } 105 | 106 | protected BinaryFormat GetBinary(int offset, int size) 107 | { 108 | TestContext.WriteLine($"{nameof(NcchConverterTests)}: {Path.GetFileName(binaryPath)}"); 109 | var stream = DataStreamFactory.FromFile(binaryPath, FileOpenMode.Read, offset, size); 110 | return new BinaryFormat(stream); 111 | } 112 | 113 | protected void ValidateHeader(Ncch actual) 114 | { 115 | var header = actual.Header; 116 | Assert.That(header.Signature, Has.Length.EqualTo(expected.SignatureLength)); 117 | } 118 | 119 | protected void ValidateRegions(Ncch actual, bool checkOffset) 120 | { 121 | Assert.That( 122 | actual.Root.Children, 123 | Has.Count.EqualTo(expected.AvailableRegions.Length)); 124 | 125 | for (int i = 0; i < actual.Root.Children.Count; i++) { 126 | var child = actual.Root.Children[i]; 127 | Assert.That(child.Name, Is.EqualTo(expected.AvailableRegions[i])); 128 | if (checkOffset) { 129 | Assert.That(child.Stream.Offset, Is.EqualTo(offset + expected.RegionsOffset[i])); 130 | } 131 | 132 | Assert.That(child.Stream.Length, Is.EqualTo(expected.RegionsSize[i]), $"Invalid region {i} size"); 133 | } 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Lemon/Titles/Binary2TitleMetadata.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.Titles 21 | { 22 | using System; 23 | using Yarhl.FileFormat; 24 | using Yarhl.IO; 25 | 26 | /// 27 | /// Deserializer of binary title metadata. 28 | /// 29 | public class Binary2TitleMetadata : IConverter 30 | { 31 | const int NumContentInfo = 64; 32 | 33 | /// 34 | /// Converts a binary format into a title metadata object. 35 | /// 36 | /// The source binary. 37 | /// The deserializer title metadata. 38 | public TitleMetadata Convert(IBinary source) 39 | { 40 | if (source == null) 41 | throw new ArgumentNullException(nameof(source)); 42 | 43 | source.Stream.Position = 0; 44 | var reader = new DataReader(source.Stream) { 45 | Endianness = EndiannessMode.BigEndian, 46 | }; 47 | 48 | TitleMetadata metadata = new TitleMetadata(); 49 | 50 | // TODO: Validate signature 51 | metadata.SignType = reader.ReadUInt32(); 52 | int signSize = GetSignatureSize(metadata.SignType); 53 | metadata.Signature = reader.ReadBytes(signSize); 54 | 55 | metadata = ReadHeader(reader, out int contentCount, metadata); 56 | 57 | for (int i = 0; i < NumContentInfo; i++) { 58 | ContentInfoRecord infoRecord = ReadInfoRecord(reader); 59 | if (!infoRecord.IsEmpty) { 60 | metadata.InfoRecords.Add(infoRecord); 61 | } 62 | } 63 | 64 | for (int i = 0; i < contentCount; i++) { 65 | ContentChunkRecord chunk = ReadChunkRecord(reader); 66 | metadata.Chunks.Add(chunk); 67 | } 68 | 69 | return metadata; 70 | } 71 | 72 | static TitleMetadata ReadHeader(DataReader reader, out int contentCount, TitleMetadata metadata) 73 | { 74 | metadata.SignatureIssuer = reader.ReadString(0x40).Replace("\0", string.Empty); 75 | metadata.Version = reader.ReadByte(); 76 | metadata.CaCrlVersion = reader.ReadByte(); 77 | metadata.SignerCrlVersion = reader.ReadByte(); 78 | reader.Stream.Position++; // padding 79 | 80 | metadata.SystemVersion = reader.ReadInt64(); 81 | metadata.TitleId = reader.ReadInt64(); 82 | metadata.TitleType = reader.ReadInt32(); 83 | metadata.GroupId = reader.ReadInt16(); 84 | 85 | metadata.SaveSize = reader.ReadInt32(); 86 | metadata.SrlPrivateSaveSize = reader.ReadInt32(); 87 | reader.Stream.Position += 4; // reserved 88 | 89 | metadata.SrlFlag = reader.ReadByte(); 90 | reader.Stream.Position += 0x31; // reserved 91 | 92 | metadata.AccessRights = reader.ReadInt32(); 93 | metadata.TitleVersion = reader.ReadInt16(); 94 | contentCount = reader.ReadInt16(); 95 | metadata.BootContent = reader.ReadInt16(); 96 | reader.Stream.Position += 2; // padding 97 | 98 | metadata.Hash = reader.ReadBytes(0x20); 99 | 100 | return metadata; 101 | } 102 | 103 | static ContentChunkRecord ReadChunkRecord(DataReader reader) 104 | { 105 | var chunk = new ContentChunkRecord { 106 | Id = reader.ReadInt32(), 107 | Index = reader.ReadInt16(), 108 | Attributes = (ContentAttributes)reader.ReadInt16(), 109 | Size = reader.ReadInt64(), 110 | Hash = reader.ReadBytes(0x20), 111 | }; 112 | 113 | return chunk; 114 | } 115 | 116 | static ContentInfoRecord ReadInfoRecord(DataReader reader) 117 | { 118 | var infoRecord = new ContentInfoRecord { 119 | IndexOffset = reader.ReadInt16(), 120 | CommandCount = reader.ReadInt16(), 121 | Hash = reader.ReadBytes(0x20), 122 | IsEmpty = false, // This exists if for some reason there's a TMD which has an empty record followed by a populated one. 123 | }; 124 | 125 | bool hashIsEmpty = true; 126 | for (int i = 0; i < infoRecord.Hash.Length; i++) { 127 | if (infoRecord.Hash[i] != 0) { 128 | hashIsEmpty = false; 129 | } 130 | } 131 | 132 | if (infoRecord.IndexOffset == 0 && infoRecord.CommandCount == 0 && hashIsEmpty) { 133 | infoRecord.IsEmpty = true; 134 | } 135 | 136 | return infoRecord; 137 | } 138 | 139 | static int GetSignatureSize(uint type) 140 | { 141 | // Including padding 142 | return type switch { 143 | 0x010000 => 0x23C, 144 | 0x010001 => 0x13C, 145 | 0x010002 => 0x7C, 146 | 0x010003 => 0x23C, 147 | 0x010004 => 0x13C, 148 | 0x010005 => 0x7C, 149 | _ => throw new NotSupportedException(), 150 | }; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Lemon/Containers/Converters/Ivfc/LevelStream.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.Containers.Converters.Ivfc 21 | { 22 | using System; 23 | using System.IO; 24 | using System.Security.Cryptography; 25 | using Yarhl.IO; 26 | using Yarhl.IO.StreamFormat; 27 | 28 | /// 29 | /// IVFC level stream. 30 | /// 31 | internal class LevelStream : StreamWrapper 32 | { 33 | readonly object lockObj = new object(); 34 | SHA256 sha; 35 | 36 | /// 37 | /// Initializes a new instance of the class 38 | /// and use a stream in-memory. 39 | /// 40 | /// Block size for padding and hash. 41 | public LevelStream(int blockSize) 42 | : this(blockSize, new RecyclableMemoryStream()) 43 | { 44 | } 45 | 46 | /// 47 | /// Initializes a new instance of the class. 48 | /// Changes in position in this stream won't affect the base stream. 49 | /// 50 | /// Block size for padding and hash. 51 | /// The underlying stream. 52 | public LevelStream(int blockSize, Stream stream) 53 | : base(new DataStream(stream)) // so the position is independent. 54 | { 55 | BlockSize = blockSize; 56 | sha = SHA256.Create(); 57 | } 58 | 59 | /// 60 | /// Raises when a new block of data is generated. 61 | /// 62 | public event EventHandler BlockWritten; 63 | 64 | /// 65 | /// Gets the block size for padding and hash. 66 | /// 67 | public int BlockSize { get; } 68 | 69 | /// 70 | /// Gets the stream lock. 71 | /// 72 | public object LockObj => lockObj; 73 | 74 | /// 75 | /// Writes a byte. 76 | /// 77 | /// Byte value. 78 | public override void WriteByte(byte value) 79 | { 80 | WriteAndUpdateHash(new[] { value }, 0, 1); 81 | } 82 | 83 | /// 84 | /// Writes the a portion of the buffer to the stream. 85 | /// 86 | /// Buffer to write. 87 | /// Index in the buffer. 88 | /// Bytes to write. 89 | public override void Write(byte[] buffer, int offset, int count) 90 | { 91 | WriteAndUpdateHash(buffer, offset, count); 92 | } 93 | 94 | /// 95 | /// Releases all resources used by the object. 96 | /// 97 | /// Whether to free the managed resources too. 98 | protected override void Dispose(bool disposing) 99 | { 100 | if (Disposed) { 101 | return; 102 | } 103 | 104 | if (disposing) { 105 | sha.Dispose(); 106 | } 107 | 108 | base.Dispose(disposing); 109 | } 110 | 111 | void WriteAndUpdateHash(byte[] data, int offset, int count) 112 | { 113 | while (true) { 114 | // Repeat until writing the data does not span into the next block. 115 | long writtenBlocks = Position / BlockSize; 116 | long futureBlocks = (Position + count) / BlockSize; 117 | if (futureBlocks == writtenBlocks) { 118 | break; 119 | } 120 | 121 | int posInBlock = (int)(Position % BlockSize); 122 | int bytesToWrite = BlockSize - posInBlock; 123 | 124 | // It's mandatory to call at least one time to TransformBlock 125 | // before calling again TransformFinalBlock, so this should 126 | // make the trick, calling with size 0. 127 | if (posInBlock == 0 && bytesToWrite > 0) 128 | { 129 | sha.TransformBlock(data, offset, 1, data, offset); 130 | WriteWithoutHash(data, offset, 1); 131 | offset += 1; 132 | count -= 1; 133 | bytesToWrite -= 1; 134 | } 135 | 136 | // Send bytes to SHA and to the stream so we update our 137 | // position too. 138 | sha.TransformFinalBlock(data, offset, bytesToWrite); 139 | WriteWithoutHash(data, offset, bytesToWrite); 140 | 141 | // Trigger event. 142 | var eventArgs = new BlockWrittenEventArgs(sha.Hash); 143 | BlockWritten?.Invoke(this, eventArgs); 144 | sha.Dispose(); 145 | sha = SHA256.Create(); 146 | 147 | // Skip already processed bytes 148 | offset += bytesToWrite; 149 | count -= bytesToWrite; 150 | } 151 | 152 | if (count > 0) { 153 | sha.TransformBlock(data, offset, count, data, offset); 154 | WriteWithoutHash(data, offset, count); 155 | } 156 | } 157 | 158 | void WriteWithoutHash(byte[] data, int index, int count) 159 | { 160 | base.Write(data, index, count); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/Lemon/Containers/Converters/BinaryCia2NodeContainer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.Containers.Converters 21 | { 22 | using System; 23 | using SceneGate.Lemon.Titles; 24 | using Yarhl.FileFormat; 25 | using Yarhl.FileSystem; 26 | using Yarhl.IO; 27 | 28 | /// 29 | /// Converter for binary CIA streams into node containers. 30 | /// 31 | public class BinaryCia2NodeContainer : IConverter 32 | { 33 | const int BlockSize = 64; 34 | 35 | /// 36 | /// Converts a binary CIA format into a container. 37 | /// 38 | /// The binary CIA format. 39 | /// The container with the CIA content. 40 | public NodeContainerFormat Convert(BinaryFormat source) 41 | { 42 | if (source == null) 43 | throw new ArgumentNullException(nameof(source)); 44 | 45 | var stream = source.Stream; 46 | Header header = ReadHeader(stream); 47 | 48 | var container = new NodeContainerFormat(); 49 | 50 | // These are for reference, they shouldn't be other than 0 in 3DS. 51 | container.Root.Tags["version"] = header.Version; 52 | container.Root.Tags["type"] = header.Type; 53 | 54 | long certsOffset = header.HeaderSize.Pad(BlockSize); 55 | Node certs = NodeFactory.FromSubstream( 56 | "certs_chain", 57 | stream, 58 | certsOffset, 59 | header.CertificatesLength); 60 | 61 | long ticketOffset = (certsOffset + header.CertificatesLength).Pad(BlockSize); 62 | Node ticket = NodeFactory.FromSubstream( 63 | "ticket", 64 | stream, 65 | ticketOffset, 66 | header.TicketLength); 67 | 68 | long tmdOffset = (ticketOffset + header.TicketLength).Pad(BlockSize); 69 | Node title = NodeFactory.FromSubstream( 70 | "title", 71 | stream, 72 | tmdOffset, 73 | header.TitleMetaLength); 74 | 75 | long contentOffset = (tmdOffset + header.TitleMetaLength).Pad(BlockSize); 76 | Node content = UnpackContent(title, stream, contentOffset); 77 | 78 | long metaOffset = (contentOffset + header.ContentLength).Pad(BlockSize); 79 | Node metadata = NodeFactory.FromSubstream( 80 | "metadata", 81 | stream, 82 | metaOffset, 83 | header.MetaLength); 84 | 85 | AddNodes(container.Root, certs, ticket, title, content, metadata); 86 | return container; 87 | } 88 | 89 | static Header ReadHeader(DataStream stream) 90 | { 91 | // Read header except content index array. 92 | // We don't need it as it's a bit array with a bit set for each 93 | // number of content (i.e. 0xC0 means two contents). 94 | var reader = new DataReader(stream); 95 | Header header = new Header { 96 | HeaderSize = reader.ReadUInt32(), 97 | Type = reader.ReadUInt16(), 98 | Version = reader.ReadUInt16(), 99 | CertificatesLength = reader.ReadUInt32(), 100 | TicketLength = reader.ReadUInt32(), 101 | TitleMetaLength = reader.ReadUInt32(), 102 | MetaLength = reader.ReadUInt32(), 103 | ContentLength = reader.ReadInt64(), 104 | }; 105 | 106 | if (header.Type != 0) 107 | throw new FormatException($"Invalid type: '{header.Type}'"); 108 | 109 | if (header.Version != 0) 110 | throw new FormatException($"Unsupported version: '{header.Version}'"); 111 | 112 | return header; 113 | } 114 | 115 | static Node UnpackContent(Node titleNode, DataStream stream, long offset) 116 | { 117 | Node content = NodeFactory.CreateContainer("content"); 118 | 119 | TitleMetadata title = titleNode.GetFormatAs().ConvertWith(new Binary2TitleMetadata()); 120 | foreach (var chunk in title.Chunks) { 121 | var chunkNode = NodeFactory.FromSubstream( 122 | chunk.GetChunkName(), 123 | stream, 124 | offset, 125 | chunk.Size); 126 | 127 | if (chunk.Attributes.HasFlag(ContentAttributes.Encrypted)) { 128 | chunkNode.Tags["LEMON_NCCH_ENCRYPTED"] = true; 129 | } 130 | 131 | content.Add(chunkNode); 132 | offset += chunk.Size; 133 | } 134 | 135 | return content; 136 | } 137 | 138 | static void AddNodes(Node parent, params Node[] children) 139 | { 140 | foreach (var child in children) { 141 | parent.Add(child); 142 | } 143 | } 144 | 145 | private struct Header 146 | { 147 | public uint HeaderSize { get; set; } 148 | 149 | public ushort Type { get; set; } 150 | 151 | public ushort Version { get; set; } 152 | 153 | public uint CertificatesLength { get; set; } 154 | 155 | public uint TicketLength { get; set; } 156 | 157 | public uint TitleMetaLength { get; set; } 158 | 159 | public uint MetaLength { get; set; } 160 | 161 | public long ContentLength { get; set; } 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Lemon/Titles/TitleMetadata2Binary.cs: -------------------------------------------------------------------------------- 1 | // TitleMetadata2Binary.cs 2 | // 3 | // Author: 4 | // Maxwell Ruiz maxwellaquaruiz@gmail.com 5 | // 6 | // Copyright (c) 2022 Maxwell Ruiz 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | namespace SceneGate.Lemon.Titles 26 | { 27 | using System.Collections.ObjectModel; 28 | using System.Security.Cryptography; 29 | using Yarhl.FileFormat; 30 | using Yarhl.IO; 31 | 32 | /// 33 | /// Convert a TitleMetadata object into binary format. 34 | /// 35 | /// 36 | /// It updates the record's hashes and TMD hash. It does not update the 37 | /// chunks (CIA content children) hashes or lengths. 38 | /// 39 | public class TitleMetadata2Binary : IConverter 40 | { 41 | private const int NumContentInfo = 64; 42 | private static readonly byte[] EmptyRecord = new byte[0x24]; 43 | 44 | /// 45 | /// Converts a title metadata object into a binary format. 46 | /// 47 | /// The object to serialize. 48 | /// The serialized title metadata. 49 | public BinaryFormat Convert(TitleMetadata source) 50 | { 51 | var binary = new BinaryFormat(); 52 | var writer = new DataWriter(binary.Stream) { 53 | Endianness = EndiannessMode.BigEndian, 54 | }; 55 | 56 | WriteHeader(source, writer); 57 | 58 | // hash placeholder 59 | writer.Stream.PushCurrentPosition(); 60 | writer.WriteTimes(0x00, 0x20); 61 | 62 | // An info record is associated with one or more "commands" or chunks. 63 | // To update the hash in the record, we need to write its chunks and hash them. 64 | // We write chunks in a separate stream as we hash them. 65 | using var chunksStream = new DataStream(); 66 | int chunksWritten = 0; 67 | 68 | long recordsInfoStart = writer.Stream.Position; 69 | long recordsLength = NumContentInfo * 0x24; 70 | for (int i = 0; i < NumContentInfo; i++) { 71 | if (i >= source.InfoRecords.Count || source.InfoRecords[i].IsEmpty) { 72 | writer.Write(EmptyRecord); 73 | continue; 74 | } 75 | 76 | WriteRecord(source.InfoRecords[i], source.Chunks, writer, chunksWritten, chunksStream); 77 | chunksWritten += source.InfoRecords[i].CommandCount; 78 | } 79 | 80 | // Write chunks 81 | chunksStream.WriteTo(writer.Stream); 82 | 83 | // Now we have the records, hash them and write it 84 | source.Hash = SHA256FromStream(writer.Stream, recordsInfoStart, recordsLength); 85 | 86 | writer.Stream.PopPosition(); 87 | writer.Write(source.Hash); 88 | 89 | return binary; 90 | } 91 | 92 | private static void WriteHeader(TitleMetadata source, DataWriter writer) 93 | { 94 | writer.Write(source.SignType); 95 | writer.Write(source.Signature); 96 | writer.Write(source.SignatureIssuer, 0x40); 97 | writer.Write(source.Version); 98 | writer.Write(source.CaCrlVersion); 99 | writer.Write(source.SignerCrlVersion); 100 | writer.Write((byte)0x0); // Reserved 101 | 102 | writer.Write(source.SystemVersion); 103 | writer.Write(source.TitleId); 104 | writer.Write(source.TitleType); 105 | writer.Write(source.GroupId); 106 | writer.Write(source.SaveSize); 107 | writer.Write(source.SrlPrivateSaveSize); 108 | writer.Write(0); // Reserved 109 | 110 | writer.Write(source.SrlFlag); 111 | writer.Write(new byte[0x31]); // Reserved 112 | 113 | writer.Write(source.AccessRights); 114 | writer.Write(source.TitleVersion); 115 | writer.Write((short)source.Chunks.Count); 116 | writer.Write((short)source.BootContent); 117 | writer.Write((short)0); // Padding 118 | } 119 | 120 | private static void WriteRecord( 121 | ContentInfoRecord record, 122 | Collection chunks, 123 | DataWriter writer, 124 | int firstChunkIndex, 125 | DataStream chunksStream) 126 | { 127 | long chunksStart = chunksStream.Position; 128 | long chunksLength = 0x30 * record.CommandCount; 129 | var chunksWriter = new DataWriter(chunksStream) { 130 | Endianness = EndiannessMode.BigEndian, 131 | }; 132 | 133 | for (int k = 0; k < record.CommandCount; k++) { 134 | ContentChunkRecord chunk = chunks[firstChunkIndex + k]; 135 | WriteChunkInfo(chunk, chunksWriter); 136 | } 137 | 138 | record.Hash = SHA256FromStream(chunksStream, chunksStart, chunksLength); 139 | 140 | writer.Write(record.IndexOffset); 141 | writer.Write(record.CommandCount); 142 | writer.Write(record.Hash); 143 | } 144 | 145 | private static void WriteChunkInfo(ContentChunkRecord chunkInfo, DataWriter writer) 146 | { 147 | writer.Write(chunkInfo.Id); 148 | writer.Write(chunkInfo.Index); 149 | writer.Write((short)chunkInfo.Attributes); 150 | writer.Write(chunkInfo.Size); 151 | writer.Write(chunkInfo.Hash); 152 | } 153 | 154 | private static byte[] SHA256FromStream(Stream stream, long offset, long length) 155 | { 156 | using var sha256 = SHA256.Create(); 157 | try { 158 | using var substream = new DataStream(stream, offset, length); 159 | return sha256.ComputeHash(substream); 160 | } catch (Exception ex) { 161 | throw new FormatException($"Failed to perform SHA256 during TMD update", ex); 162 | } 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/Lemon/Containers/Converters/BinaryExeFs2NodeContainer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.Containers.Converters 21 | { 22 | using System; 23 | using System.Diagnostics.CodeAnalysis; 24 | using System.IO; 25 | using System.Linq; 26 | using System.Security.Cryptography; 27 | using System.Text; 28 | using Microsoft.Extensions.Logging; 29 | using Yarhl.FileFormat; 30 | using Yarhl.FileSystem; 31 | using Yarhl.IO; 32 | 33 | /// 34 | /// Converter for Binary streams into a file system following the 35 | /// Executable File System tree format. 36 | /// 37 | public class BinaryExeFs2NodeContainer : 38 | IConverter, 39 | IConverter 40 | { 41 | private const int NumberFiles = 10; 42 | private const int HeaderSize = 0x200; 43 | private const int Sha256Size = 0x20; 44 | private readonly ILogger logger; 45 | 46 | /// 47 | /// Initializes a new instance of the class. 48 | /// 49 | public BinaryExeFs2NodeContainer() 50 | { 51 | logger = LoggerFactory.Instance.CreateLogger(); 52 | } 53 | 54 | /// 55 | /// Converts a binary stream with Executable file system into nodes. 56 | /// 57 | /// The binary stream to convert. 58 | /// The file system from the ExeFS stream. 59 | [SuppressMessage("Reliability", "CA2000", Justification = "Transfer ownership")] 60 | public NodeContainerFormat Convert(BinaryFormat source) 61 | { 62 | ArgumentNullException.ThrowIfNull(source); 63 | 64 | var reader = new DataReader(source.Stream) { 65 | DefaultEncoding = Encoding.ASCII, 66 | }; 67 | var container = new NodeContainerFormat(); 68 | 69 | for (int i = 0; i < NumberFiles; i++) { 70 | Node child = GetNodeFromHeader(reader); 71 | if (child != null) { 72 | container.Root.Add(child); 73 | } 74 | } 75 | 76 | reader.SkipPadding(0x40); 77 | 78 | // Validate the hashes 79 | int totalHashSize = NumberFiles * Sha256Size; 80 | for (int i = 0; i < container.Root.Children.Count; i++) { 81 | // Hash are stored in reverse order, from end to beginning 82 | byte[] expected = null; 83 | source.Stream.RunInPosition( 84 | () => expected = reader.ReadBytes(Sha256Size), 85 | totalHashSize - (Sha256Size * (i + 1)), 86 | SeekOrigin.Current); 87 | 88 | byte[] actual = ComputeHash(container.Root.Children[i].Stream); 89 | 90 | if (!expected.SequenceEqual(actual)) { 91 | logger.LogWarning("Wrong hash for child {Name}", container.Root.Children[i].Name); 92 | } 93 | } 94 | 95 | return container; 96 | } 97 | 98 | /// 99 | /// Converts a container of nodes into a binary of format 100 | /// Executable file system. 101 | /// 102 | /// Container of nodes. 103 | /// The new binary container. 104 | public BinaryFormat Convert(NodeContainerFormat source) 105 | { 106 | ArgumentNullException.ThrowIfNull(source); 107 | 108 | var binary = new BinaryFormat(); 109 | var writer = new DataWriter(binary.Stream) { 110 | DefaultEncoding = Encoding.ASCII, 111 | }; 112 | 113 | // Generate empty header so we can write content at the same time 114 | writer.WriteTimes(0x00, HeaderSize); 115 | 116 | // Write table with files 117 | binary.Stream.Position = 0; 118 | foreach (var child in source.Root.Children) { 119 | writer.Write(child.Name, 8); 120 | writer.Write((uint)(binary.Stream.Length - HeaderSize)); 121 | writer.Write((uint)child.Stream.Length); 122 | 123 | binary.Stream.RunInPosition( 124 | () => { 125 | child.Stream.WriteTo(binary.Stream); 126 | writer.WritePadding(0x00, 0x200); 127 | }, 128 | 0, 129 | SeekOrigin.End); 130 | } 131 | 132 | binary.Stream.Position = NumberFiles * 0x10; 133 | writer.WritePadding(0x00, 0x40); 134 | 135 | int totalHashSize = NumberFiles * Sha256Size; 136 | for (int i = 0; i < source.Root.Children.Count; i++) { 137 | byte[] hash = ComputeHash(source.Root.Children[i].Stream); 138 | 139 | // Hash are stored in reverse order, from end to beginning 140 | binary.Stream.RunInPosition( 141 | () => writer.Write(hash), 142 | totalHashSize - (Sha256Size * (i + 1)), 143 | SeekOrigin.Current); 144 | } 145 | 146 | return binary; 147 | } 148 | 149 | [SuppressMessage("Reliability", "CA2000", Justification = "Transfer ownership")] 150 | private static Node GetNodeFromHeader(DataReader reader) 151 | { 152 | string name = reader.ReadString(8).Replace("\0", string.Empty); 153 | uint offset = reader.ReadUInt32() + HeaderSize; 154 | uint size = reader.ReadUInt32(); 155 | 156 | Node node = null; 157 | if (!string.IsNullOrEmpty(name)) { 158 | node = new Node(name, new BinaryFormat(reader.Stream, offset, size)); 159 | } 160 | 161 | return node; 162 | } 163 | 164 | private static byte[] ComputeHash(DataStream stream) 165 | { 166 | byte[] hash; 167 | using (SHA256 sha = SHA256.Create()) { 168 | // Read the file in blocks of 64 KB (small enough for the SOH). 169 | stream.Position = 0; 170 | int offset = 0; 171 | int read; 172 | byte[] buffer = new byte[1024 * 64]; 173 | while (offset + buffer.Length < stream.Length) { 174 | read = stream.Read(buffer, 0, buffer.Length); 175 | offset += sha.TransformBlock(buffer, 0, read, buffer, 0); 176 | } 177 | 178 | read = stream.Read(buffer); 179 | sha.TransformFinalBlock(buffer, 0, read); 180 | 181 | hash = sha.Hash; 182 | } 183 | 184 | return hash; 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/Lemon/Containers/Converters/BinaryIvfc2NodeContainer.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.Containers.Converters 21 | { 22 | using System; 23 | using System.Diagnostics.CodeAnalysis; 24 | using System.Text; 25 | using Microsoft.Extensions.Logging; 26 | using Yarhl.FileFormat; 27 | using Yarhl.FileSystem; 28 | using Yarhl.IO; 29 | 30 | /// 31 | /// Converter for Binary streams into a file system following the 32 | /// IVFC tree format. 33 | /// 34 | /// 35 | /// This converter does not validate the level 0, 1 and 2 hashes 36 | /// or use the level 3 file and directory tokens. 37 | /// 38 | public class BinaryIvfc2NodeContainer : IConverter 39 | { 40 | private const int Level0Padding = 0x1000; 41 | 42 | private static readonly Encoding Encoding = Encoding.Unicode; 43 | private readonly ILogger logger; 44 | 45 | private DataReader levelReader; 46 | private uint dirInfoOffset; 47 | private uint fileInfoOffset; 48 | private uint fileDataOffset; 49 | 50 | /// 51 | /// Initializes a new instance of the class. 52 | /// 53 | public BinaryIvfc2NodeContainer() 54 | { 55 | logger = LoggerFactory.Instance.CreateLogger(); 56 | } 57 | 58 | /// 59 | /// Gets the magic identifier of the format. 60 | /// 61 | /// The magic ID of the format. 62 | public static string MagicId => "IVFC"; 63 | 64 | /// 65 | /// Gets the supported format version. 66 | /// 67 | /// The supported format version. 68 | public static uint SupportedVersion => 0x0001_0000; 69 | 70 | /// 71 | /// Converts a binary stream into a file system with the IVFC format. 72 | /// 73 | /// The binary stream to convert. 74 | /// The file system from the IVFC stream. 75 | public NodeContainerFormat Convert(BinaryFormat source) 76 | { 77 | ArgumentNullException.ThrowIfNull(source); 78 | 79 | var reader = new DataReader(source.Stream); 80 | var root = new NodeContainerFormat(); 81 | 82 | if (reader.ReadString(4) != MagicId) 83 | throw new FormatException("Invalid Magic ID"); 84 | 85 | uint version = reader.ReadUInt32(); 86 | if (version != SupportedVersion) { 87 | logger.LogWarning("Unsupported version: {Actual}, expecting {Supported}.", version, SupportedVersion); 88 | } 89 | 90 | // Level 0, 1 and 2 only contain SHA-256 hashes. We can skip 91 | // and go directly to level 3 where the directory and file data is. 92 | // We don't need either the offset and size from the levels. 93 | uint level0Size = reader.ReadUInt32(); 94 | reader.Stream.Position = 0x44; 95 | uint level3Size = reader.ReadUInt32(); 96 | reader.Stream.Position = 0x54; 97 | uint headerSize = reader.ReadUInt32(); 98 | 99 | // Level 3 is right after level 0. Don't ask me why -.-' 100 | // We know it should be call level 3 and not level 1 because the 101 | // logical level offset entries from the header follow that order. 102 | // We create a new sub-stream because all the offset are relative 103 | // to this section. 104 | long level3Offset = (headerSize + level0Size).Pad(Level0Padding); 105 | using (var level3 = DataStreamFactory.FromStream(reader.Stream, level3Offset, level3Size)) { 106 | levelReader = new DataReader(level3); 107 | 108 | // First we have the header. Since we don't need to search an 109 | // entry but we read all of them, we can skip the token hashes, 110 | // and go directly to the information sections. 111 | level3.Position = 0x0C; 112 | dirInfoOffset = levelReader.ReadUInt32(); 113 | level3.Position = 0x1C; 114 | fileInfoOffset = levelReader.ReadUInt32(); 115 | level3.Position = 0x24; 116 | fileDataOffset = levelReader.ReadUInt32(); 117 | 118 | // Start processing directories and from there we will 119 | // get their files too. First entry is root. 120 | level3.Position = dirInfoOffset; 121 | ReadDirectoryInfo(root.Root); 122 | } 123 | 124 | return root; 125 | } 126 | 127 | [SuppressMessage("Reliability", "CA2000", Justification = "Transfer ownership")] 128 | private void ReadDirectoryInfo(Node parent) 129 | { 130 | levelReader.Stream.Position += 4; // no need parent directory 131 | uint nextSiblingDir = levelReader.ReadUInt32(); 132 | uint firstChildDir = levelReader.ReadUInt32(); 133 | uint firstChildFile = levelReader.ReadUInt32(); 134 | levelReader.Stream.Position += 4; // we are not using the hash table 135 | int nameLength = levelReader.ReadInt32(); 136 | 137 | // We pass the root node, so detect if it's that to reuse object. 138 | // In any case, the root node doesn't have name (nameLength == 0). 139 | Node current; 140 | if (nameLength == 0) { 141 | if (parent.Parent != null) { 142 | logger.LogError("Directory without name. Ignoring."); 143 | return; 144 | } 145 | 146 | current = parent; 147 | } else { 148 | string name = Encoding.GetString(levelReader.ReadBytes(nameLength)); 149 | current = NodeFactory.CreateContainer(name); 150 | parent.Add(current); 151 | } 152 | 153 | // Get next sibling or child files / dirs. 154 | if (nextSiblingDir != 0xFFFFFFFF) { 155 | levelReader.Stream.Position = dirInfoOffset + nextSiblingDir; 156 | ReadDirectoryInfo(parent); 157 | } 158 | 159 | if (firstChildFile != 0xFFFFFFFF) { 160 | levelReader.Stream.Position = fileInfoOffset + firstChildFile; 161 | ReadFileInfo(current); 162 | } 163 | 164 | if (firstChildDir != 0xFFFFFFFF) { 165 | levelReader.Stream.Position = dirInfoOffset + firstChildDir; 166 | ReadDirectoryInfo(current); 167 | } 168 | } 169 | 170 | private void ReadFileInfo(Node parent) 171 | { 172 | levelReader.Stream.Position += 4; // no need parent directory 173 | uint nextSiblingFile = levelReader.ReadUInt32(); 174 | long offset = levelReader.ReadInt64() + fileDataOffset; 175 | long size = levelReader.ReadInt64(); 176 | levelReader.Stream.Position += 4; // we are not using the hash table 177 | int nameLength = levelReader.ReadInt32(); 178 | string name = Encoding.GetString(levelReader.ReadBytes(nameLength)); 179 | 180 | var file = NodeFactory.FromSubstream(name, levelReader.Stream, offset, size); 181 | parent.Add(file); 182 | 183 | if (nextSiblingFile != 0xFFFFFFFF) { 184 | levelReader.Stream.Position = fileInfoOffset + nextSiblingFile; 185 | ReadFileInfo(parent); 186 | } 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Lemon/Containers/Converters/NodeContainer2BinaryIvfc.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2019 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.Containers.Converters 21 | { 22 | using System; 23 | using SceneGate.Lemon.Containers.Converters.Ivfc; 24 | using Yarhl.FileFormat; 25 | using Yarhl.FileSystem; 26 | using Yarhl.IO; 27 | 28 | /// 29 | /// Converter for Binary streams into a file system following the 30 | /// IVFC tree format. 31 | /// 32 | /// 33 | /// The binary IVFC format consists in the following sections: 34 | /// * IVFC Header 35 | /// * Level 0 36 | /// * Level 3 37 | /// |--* File system header 38 | /// |--* Directories hash 39 | /// |--* Directories info 40 | /// |--* Files hash 41 | /// |--* Files info 42 | /// |--* File data 43 | /// * Level 1 44 | /// * Level 2. 45 | /// Level 0, 1 and 2 only contain SHA-256 hashes of the upper layer. 46 | /// Level 3 contains the file system metadata and file data. 47 | /// 48 | public class NodeContainer2BinaryIvfc : IConverter 49 | { 50 | const int BlockSizeLog = 0x0C; 51 | const int BlockSize = 1 << BlockSizeLog; 52 | 53 | private readonly DataStream stream; 54 | 55 | /// 56 | /// Initializes a new instance of the class. 57 | /// 58 | public NodeContainer2BinaryIvfc() 59 | { 60 | } 61 | 62 | /// 63 | /// Initializes a new instance of the class. 64 | /// 65 | /// Stream to write to. 66 | public NodeContainer2BinaryIvfc(DataStream outputStream) 67 | { 68 | stream = outputStream; 69 | } 70 | 71 | /// 72 | /// Gets the magic identifier of the format. 73 | /// 74 | /// The magic ID of the format. 75 | public static string MagicId { 76 | get { return "IVFC"; } 77 | } 78 | 79 | /// 80 | /// Gets the format version. 81 | /// 82 | /// The format version. 83 | public static uint Version { 84 | get { return 0x0001_0000; } 85 | } 86 | 87 | /// 88 | /// Converts a file system into a memory binary stream with IVFC format. 89 | /// 90 | /// The node file system to convert. 91 | /// The memory binary stream with IVFC format. 92 | public BinaryFormat Convert(NodeContainerFormat source) 93 | { 94 | if (source == null) 95 | throw new ArgumentNullException(nameof(source)); 96 | 97 | var binary = (stream != null) ? new BinaryFormat(stream) : new BinaryFormat(); 98 | var writer = new DataWriter(binary.Stream); 99 | 100 | // Analyze the file system to pre-calculate the sizes. 101 | // As said, Level 3 contains the whole file system, 102 | // the other level just contain SHA-256 hashes of every block 103 | // from the lower layer. So we get the number of blocks 104 | // (number of hashes) and multiple by the hash size. 105 | var fsWriter = new FileSystemWriter(source.Root); 106 | 107 | const int LevelHashSize = 0x20; // SHA-256 size 108 | long[] levelSizes = new long[4]; 109 | levelSizes[3] = fsWriter.Size; 110 | levelSizes[2] = (levelSizes[3].Pad(BlockSize) / BlockSize) * LevelHashSize; 111 | levelSizes[1] = (levelSizes[2].Pad(BlockSize) / BlockSize) * LevelHashSize; 112 | levelSizes[0] = (levelSizes[1].Pad(BlockSize) / BlockSize) * LevelHashSize; 113 | 114 | WriteHeader(writer, levelSizes); 115 | writer.WritePadding(0x00, 0x10); 116 | 117 | long level0DataOffset = binary.Stream.Position; 118 | writer.WriteTimes(0x00, levelSizes[0]); // pre-allocate 119 | writer.WritePadding(0x00, BlockSize); 120 | long level3Offset = binary.Stream.Position; 121 | 122 | // Increase the base length so we can create a substream of the correct size 123 | // This operation doesn't write and it returns almost immediately, 124 | // the write happens on the first byte written on the new file space. 125 | long level3Padded = levelSizes[3].Pad(BlockSize); 126 | binary.Stream.BaseStream.SetLength(binary.Stream.BaseStream.Length + level3Padded); 127 | binary.Stream.SetLength(binary.Stream.Length + level3Padded); 128 | 129 | // Create "special" data streams that will create hashes on-the-fly. 130 | using (var level1 = new LevelStream(BlockSize)) 131 | using (var level2 = new LevelStream(BlockSize)) 132 | using (var level3 = new LevelStream(BlockSize, binary.Stream.BaseStream)) 133 | using (var level0Stream = new DataStream(binary.Stream, level0DataOffset, levelSizes[0])) 134 | using (var level1Stream = new DataStream(level1)) 135 | using (var level2Stream = new DataStream(level2)) 136 | using (var level3Stream = new DataStream(level3, level3Offset, level3Padded, true)) { 137 | level3.BlockWritten += (_, e) => level2Stream.Write(e.Hash, 0, e.Hash.Length); 138 | level2.BlockWritten += (_, e) => level1Stream.Write(e.Hash, 0, e.Hash.Length); 139 | level1.BlockWritten += (_, e) => level0Stream.Write(e.Hash, 0, e.Hash.Length); 140 | 141 | fsWriter.Write(level3Stream); 142 | 143 | // Pad remaining block size in order. 144 | new DataWriter(level3Stream).WritePadding(0x00, BlockSize); 145 | new DataWriter(level2Stream).WritePadding(0x00, BlockSize); 146 | new DataWriter(level1Stream).WritePadding(0x00, BlockSize); 147 | 148 | // Write streams in order. 149 | binary.Stream.Position = binary.Stream.Length; 150 | level1Stream.WriteTo(binary.Stream); 151 | level2Stream.WriteTo(binary.Stream); 152 | } 153 | 154 | return binary; 155 | } 156 | 157 | static void WriteHeader(DataWriter writer, long[] sizes) 158 | { 159 | const uint HeaderSize = 0x5C; 160 | 161 | // Calculate the "logical" offset. 162 | // This does not reflect the offset in the actual file, 163 | // but how it would be if the layers were in order. 164 | long level1LogicalOffset = 0x00; // first level 165 | long level2LogicalOffset = level1LogicalOffset + sizes[1].Pad(BlockSize); 166 | long level3LogicalOffset = level2LogicalOffset + sizes[2].Pad(BlockSize); 167 | 168 | writer.Write(MagicId, nullTerminator: false); 169 | writer.Write(Version); 170 | writer.Write((int)sizes[0]); 171 | 172 | writer.Write(level1LogicalOffset); 173 | writer.Write(sizes[1]); 174 | writer.Write(BlockSizeLog); 175 | writer.Write(0x00); // reserved 176 | 177 | writer.Write(level2LogicalOffset); 178 | writer.Write(sizes[2]); 179 | writer.Write(BlockSizeLog); 180 | writer.Write(0x00); // reserved 181 | 182 | writer.Write(level3LogicalOffset); 183 | writer.Write(sizes[3]); 184 | writer.Write(BlockSizeLog); 185 | writer.Write(0x00); // reserved 186 | 187 | writer.Write(HeaderSize); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/Lemon/Containers/Converters/NodeContainer2BinaryCia.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 SceneGate 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in all 11 | // copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | // SOFTWARE. 20 | namespace SceneGate.Lemon.Containers.Converters 21 | { 22 | using System; 23 | using System.Security.Cryptography; 24 | using SceneGate.Lemon.Titles; 25 | using Yarhl.FileFormat; 26 | using Yarhl.FileSystem; 27 | using Yarhl.IO; 28 | 29 | /// 30 | /// Convert a NodeContainer with CIA sections into binary format. 31 | /// 32 | /// 33 | ///

The initialization is optional. If no stream is provided, a new one 34 | /// will be created on memory. This may consume large RAM memory for big games.

35 | ///

This converter expects to have a node with the following binary 36 | /// children: certs_chain, ticket, title (TMD), content/program, 37 | /// content/manual (optional), content/download_play (optional).

38 | ///

This converter will update the hashes of the TMD except if the 39 | /// content is encrypted.

40 | ///
41 | public class NodeContainer2BinaryCia : IConverter 42 | { 43 | private const int BlockSize = 64; 44 | private const int ContentIndexSize = 0x2000; 45 | private const int HeaderSize = 0x20 + ContentIndexSize; 46 | private const ushort CiaType = 0; // the type of format, always 0 for 3DS 47 | private const ushort CiaVersion = 0; // the version of the format 48 | 49 | private readonly DataStream outputStream; 50 | 51 | /// 52 | /// Initializes a new instance of the class. 53 | /// 54 | public NodeContainer2BinaryCia() 55 | { 56 | } 57 | 58 | /// 59 | /// Initializes a new instance of the class. 60 | /// 61 | /// The stream to write the new CIA. 62 | public NodeContainer2BinaryCia(DataStream parameters) 63 | { 64 | outputStream = parameters; 65 | } 66 | 67 | /// 68 | /// Converts a container into a binary CIA. 69 | /// 70 | /// The container with the CIA files. 71 | /// The new binary format with the CIA. 72 | public BinaryFormat Convert(NodeContainerFormat source) 73 | { 74 | ArgumentNullException.ThrowIfNull(source); 75 | 76 | var binary = (outputStream != null) ? new BinaryFormat(outputStream) : new BinaryFormat(); 77 | binary.Stream.Position = 0; 78 | 79 | var writer = new DataWriter(binary.Stream) { 80 | Endianness = EndiannessMode.LittleEndian, 81 | }; 82 | 83 | Node root = source.Root; 84 | 85 | if (root.Children["title"].Format is IBinary) { 86 | root.Children["title"].TransformWith(); 87 | } 88 | 89 | if (root.Children["title"].Format is not TitleMetadata) { 90 | throw new FormatException("Unknown format for title node. It must be IBinary or TitleMetadata."); 91 | } 92 | 93 | var title = root.Children["title"].GetFormatAs(); 94 | UpdateTitleMetadata(title, root.Children["content"]); 95 | root.Children["title"].TransformWith(); 96 | 97 | WriteHeader(writer, root); 98 | 99 | WriteFile(writer, root.Children["certs_chain"]); 100 | WriteFile(writer, root.Children["ticket"]); 101 | WriteFile(writer, root.Children["title"]); 102 | 103 | WriteContent(writer, root.Children["content"], title); 104 | WriteFile(writer, root.Children["metadata"], false); 105 | 106 | return binary; 107 | } 108 | 109 | private static void WriteHeader(DataWriter writer, Node root) 110 | { 111 | writer.Write(HeaderSize); 112 | writer.Write(CiaType); 113 | writer.Write(CiaVersion); 114 | writer.Write((uint)(root.Children["certs_chain"]?.Stream?.Length ?? 0)); 115 | writer.Write((uint)(root.Children["ticket"]?.Stream?.Length ?? 0)); 116 | writer.Write((uint)(root.Children["title"]?.Stream?.Length ?? 0)); 117 | writer.Write((uint)(root.Children["metadata"]?.Stream?.Length ?? 0)); 118 | writer.Write(0L); // placeholder for content size 119 | writer.WriteTimes(0, ContentIndexSize); // placeholder for content index 120 | writer.WritePadding(0x00, BlockSize); 121 | } 122 | 123 | private static void WriteFile(DataWriter writer, Node file, bool isRequired = true) 124 | { 125 | if (file == null && isRequired) { 126 | throw new FileNotFoundException("Missing CIA file"); 127 | } 128 | 129 | if (file is null) { 130 | return; 131 | } 132 | 133 | if (file.Format is not IBinary) { 134 | throw new FormatException("Cannot write file as it is not binary"); 135 | } 136 | 137 | file.Stream.WriteTo(writer.Stream); 138 | writer.WritePadding(0x00, BlockSize); 139 | } 140 | 141 | private static void UpdateTitleMetadata(TitleMetadata title, Node content) 142 | { 143 | // Update size and HASH title chunks (CIA content children) 144 | // The hashes of the records and TMD will be updated when writing. 145 | for (int i = 0; i < title.Chunks.Count; i++) { 146 | var chunk = title.Chunks[i]; 147 | 148 | string childName = title.Chunks[i].GetChunkName(); 149 | Node child = content.Children[childName] 150 | ?? throw new FormatException($"Missing child: {childName}"); 151 | 152 | if (child.Format is not IBinary) { 153 | throw new FormatException($"Cannot write child {childName} as it is not binary"); 154 | } 155 | 156 | chunk.Size = child.Stream.Length; 157 | 158 | // At this moment we cannot re-hash encrypted content. 159 | // The hash is over the decrypted content and we don't support 160 | // decryption / encryption. 161 | if (!chunk.Attributes.HasFlag(ContentAttributes.Encrypted)) { 162 | chunk.Hash = SHA256FromStream(child.Stream); 163 | } 164 | } 165 | } 166 | 167 | private static byte[] SHA256FromStream(DataStream stream) 168 | { 169 | using var sha256 = SHA256.Create(); 170 | try { 171 | stream.Position = 0; 172 | return sha256.ComputeHash(stream); 173 | } catch (Exception ex) { 174 | throw new FormatException($"Failed to perform SHA256 during TMD update", ex); 175 | } 176 | } 177 | 178 | private static void WriteContent(DataWriter writer, Node content, TitleMetadata title) 179 | { 180 | int contentBitset = 0; 181 | long contentSize = 0; 182 | for (int i = 0; i < title.Chunks.Count; i++) { 183 | string childName = title.Chunks[i].GetChunkName(); 184 | var child = content.Children[childName] 185 | ?? throw new FormatException($"Missing child: {childName}"); 186 | 187 | if (child.Format is not IBinary) { 188 | throw new FormatException($"Cannot write child {childName} as it is not binary"); 189 | } 190 | 191 | child.Stream.WriteTo(writer.Stream); 192 | contentSize += child.Stream.Length; 193 | contentBitset |= 1 << (7 - i); // one high bit per content 194 | } 195 | 196 | // Update header values 197 | writer.Stream.PushToPosition(0x18); 198 | writer.Write(contentSize); 199 | writer.Write(contentBitset); 200 | writer.Stream.PopPosition(); 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/Lemon/Containers/Converters/Ncch2Binary.cs: -------------------------------------------------------------------------------- 1 | // Ncch2Binary.cs 2 | // 3 | // Author: 4 | // Maxwell Ruiz maxwellaquaruiz@gmail.com 5 | // 6 | // Copyright (c) 2022 Maxwell Ruiz 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | namespace SceneGate.Lemon.Containers.Converters 26 | { 27 | using System; 28 | using System.Security.Cryptography; 29 | using SceneGate.Lemon.Containers.Formats; 30 | using Yarhl.FileFormat; 31 | using Yarhl.FileSystem; 32 | using Yarhl.IO; 33 | 34 | /// 35 | /// Convert a NCCH instance into binary format. 36 | /// 37 | /// 38 | ///

This converter expects to have a node with the following binary 39 | /// children: 40 | /// extended_header (optional), access_descriptor (optional), sdk_info.txt (optional), 41 | /// logo.bin (optional), system (optional), rom (optional).

42 | ///
43 | public class Ncch2Binary : IConverter 44 | { 45 | private readonly DataStream stream; 46 | 47 | /// 48 | /// Initializes a new instance of the class. 49 | /// 50 | public Ncch2Binary() 51 | { 52 | stream = null; 53 | } 54 | 55 | /// 56 | /// Initializes a new instance of the class. 57 | /// 58 | /// Stream to write to. 59 | public Ncch2Binary(DataStream stream) 60 | { 61 | ArgumentNullException.ThrowIfNull(stream); 62 | this.stream = stream; 63 | } 64 | 65 | /// 66 | /// Converts a NCCH instance into a binary stream. 67 | /// 68 | /// Ncch to convert. 69 | /// The new binary container. 70 | public BinaryFormat Convert(Ncch source) 71 | { 72 | ArgumentNullException.ThrowIfNull(source); 73 | 74 | var binary = (stream != null) ? new BinaryFormat(stream) : new BinaryFormat(); 75 | var writer = new DataWriter(binary.Stream); 76 | 77 | Node root = source.Root; 78 | 79 | // Ncch header data has a length of 0x200 before binary data. 80 | writer.Write(new byte[0x200]); 81 | writer.Stream.Position = 0; 82 | 83 | // First we write the header signature 84 | writer.Write(source.Header.Signature); 85 | 86 | byte[] magicId = new byte[] { 0x4E, 0x43, 0x43, 0x48 }; 87 | writer.Write(magicId); 88 | 89 | // We'll write the total size at last. 90 | writer.Stream.Position += 0x4; 91 | 92 | writer.Write(source.Header.PartitionId); 93 | writer.Write(source.Header.MakerCode); 94 | writer.Write(source.Header.Version); 95 | 96 | if (source.Header.Flags[7] == 0x20) { 97 | throw new FormatException("Value 0x20 for Flag 0x07 not supported yet"); 98 | } 99 | else { 100 | writer.Stream.Position += 0x4; 101 | } 102 | 103 | writer.Write(source.Header.ProgramId); 104 | 105 | writer.Stream.Position += 0x10; // Reserved 106 | 107 | WriteSHA256(writer, root.Children["logo.bin"]); 108 | writer.Write(source.Header.ProductCode.PadRight(0x10, '\0')); 109 | WriteSHA256(writer, root.Children["extended_header"]); 110 | writer.Write((int)(root.Children["extended_header"]?.Stream?.Length ?? 0)); 111 | 112 | writer.Stream.Position += 0x4; // Reserved 113 | 114 | for (int i = 0; i < source.Header.Flags.Length; i++) { 115 | byte flag = source.Header.Flags[i]; 116 | writer.Write(flag); 117 | } 118 | 119 | // Now we begin writing the NCCH content 120 | writer.Stream.Position = 0x200; 121 | WriteFile(writer, root.Children["extended_header"], false); 122 | WriteFile(writer, root.Children["access_descriptor"], false); 123 | 124 | writer.Stream.Position = 0x190; 125 | WriteOffsetSizeAndData(writer, root.Children["sdk_info.txt"]); 126 | WriteOffsetSizeAndData(writer, root.Children["logo.bin"]); 127 | WriteOffsetSizeAndData(writer, root.Children["system"], source.Header.SystemHashSize, true); 128 | 129 | writer.Stream.Position += 0x4; // Reserved 130 | WriteOffsetSizeAndData(writer, root.Children["rom"], source.Header.RomHashSize, true); 131 | 132 | writer.Stream.Position += 0x4; // Reserved 133 | 134 | WriteSHA256(writer, root.Children["system"], source.Header.SystemHashSize, true); 135 | WriteSHA256(writer, root.Children["rom"], source.Header.RomHashSize, true); 136 | 137 | // Write padding at the end of the binary to prevent having a wrong size in units 138 | writer.Stream.Position = writer.Stream.Length; 139 | writer.WritePadding(0, NcchHeader.Unit); 140 | 141 | // Write the full size of the binary (in units) in 0x104 142 | writer.Stream.Position = 0x104; 143 | writer.Write((int)writer.Stream.Length / NcchHeader.Unit); 144 | 145 | return binary; 146 | } 147 | 148 | private static void WriteFile(DataWriter writer, Node file, bool isRequired = true) 149 | { 150 | if (file == null && isRequired) { 151 | throw new FileNotFoundException("Missing NCCH file"); 152 | } else if (file == null) { 153 | return; 154 | } 155 | 156 | if (file.Format is not IBinary) { 157 | throw new FormatException("Cannot write file as it is not binary"); 158 | } 159 | 160 | file.Stream.WriteTo(writer.Stream); 161 | } 162 | 163 | private static void WriteOffsetSizeAndData( 164 | DataWriter writer, 165 | Node file, 166 | int hashRegion = 0, 167 | bool hasHashRegion = false) 168 | { 169 | if (file != null) { 170 | long position = writer.Stream.Position; 171 | int offsetInUnits = (int)writer.Stream.Length / NcchHeader.Unit; 172 | int fileLength = (int)file.Stream.Length.Pad(NcchHeader.Unit); 173 | int fileLengthInUnits = fileLength / NcchHeader.Unit; 174 | 175 | writer.Write(offsetInUnits); 176 | writer.Write(fileLengthInUnits); 177 | 178 | writer.Stream.Position = writer.Stream.Length; 179 | file.Stream.WriteTo(writer.Stream); 180 | writer.WritePadding(0, NcchHeader.Unit); 181 | 182 | writer.Stream.Position = position + 0x8; 183 | } 184 | else { 185 | writer.Write(0); 186 | writer.Write(0); 187 | } 188 | 189 | if (hasHashRegion) { 190 | writer.Write(hashRegion); 191 | } 192 | } 193 | 194 | private static void WriteSHA256( 195 | DataWriter writer, 196 | Node file, 197 | int hashRegion = 0, 198 | bool hasHashRegion = false) 199 | { 200 | if (file is null) { 201 | writer.Write(new byte[0x20]); 202 | return; 203 | } 204 | 205 | using var sha256 = SHA256.Create(); 206 | try { 207 | 208 | if (hasHashRegion && hashRegion == 0) { 209 | writer.Write(sha256.ComputeHash(Array.Empty())); 210 | } else if (hasHashRegion) { 211 | int hashSize = hashRegion * 0x200; 212 | byte[] buffer = new byte[hashSize]; 213 | file.Stream.Position = 0; 214 | int read = file.Stream.Read(buffer, 0, hashSize); 215 | if (read != hashSize) { 216 | throw new EndOfStreamException(); 217 | } 218 | 219 | writer.Write(sha256.ComputeHash(buffer)); 220 | } else { 221 | writer.Write(sha256.ComputeHash(file.Stream)); 222 | } 223 | } catch (Exception ex) { 224 | throw new FormatException($"Failed to perform SHA256 for {file.Name}", ex); 225 | } 226 | } 227 | } 228 | } 229 | --------------------------------------------------------------------------------