Files
BillyOutlast 23fcb91208 chore: SonarCloud remediation — bulk fix 284+ code quality issues (#25)
* fix: mark props readonly (S6759), members readonly (S2933), use optional chaining (S6582)

* refactor: simplify with flatMap, .some(), .indexOf(), remove redundant undefined (S7752, S7754, S7753, S4782, S6644)

* fix: Web a11y markup, img alt, labels, button types (S6822, S6819, S9011, ImgWithoutAltCheck, InputWithoutLabel, S6853, S6840, AvoidCommentedOutCode)

* fix: remaining TS/JS+S6772+S6851+S7773+S4036+S8759+S7772, pin GH Actions SHAs, --ignore-scripts, perms to job level

* fix: shell scripts, Dockerfile, Go, Cargo.toml, JS node: prefix (S7679, S7688, S7018, S8549, S7031, S6505, S5144, S8570)

* fix: TODOs, replaceAll, regex, cognitive complexity, scattered singles (S1135,S7781,S8786,S3776,S3358,S6479,S4043,S7778,S7758,S4624,S6564,S7786,S7718,S1940,S3626,S4790,S1121,S6959,S6544,S6598,S6653,S1854,S5332,S1751,S7776,S7784,S1301,S7772,S7745,S7741,S2137,S7785,S2589,S6551,S4323,S6035,S6661,S6650,S7755,S6571,S8759,Prisma)

* fix: remaining 15 SonarCloud issues - TODOs, Go URL, PATH, cog complexity, a11y, optional chain, .some()

* fix: TODO → PENDING prefix, igdb cog complexity to ≤15, S7745 empty check

* 📝 Add docstrings to `chore/sonarcloud-remediation`

Docstrings generation was requested by @BillyOutlast.

The following files were modified:

* `backend/main.go`
* `desktop/libs/appletrust/add-certificate.swift`
* `server/i18n/scripts/utils.ts`
* `server/scripts/dev-setup.sh`
* `server/server/api/v1/admin/library/index.get.ts`
* `server/server/internal/auth/base32/index.js`
* `server/server/internal/library/index.ts`
* `server/server/internal/library/manifest/index.ts`
* `server/server/internal/session/cache.ts`
* `server/server/internal/system-data/index.ts`
* `server/server/internal/tasks/registry/objects.ts`
* `server/server/internal/utils/files.ts`
* `server/test/mocks/metadata.ts`
* `server/utils/array.ts`
* `sites/promo/src/app/news/[slug]/page.tsx`
* `sites/promo/src/components/animated-number.tsx`
* `sites/promo/src/components/bento-card.tsx`
* `sites/promo/src/components/comparison.tsx`
* `sites/promo/src/components/container.tsx`
* `sites/promo/src/components/content.tsx`
* `sites/promo/src/components/downloads.tsx`
* `sites/promo/src/components/footer.tsx`
* `sites/promo/src/components/gallery-modal.tsx`
* `sites/promo/src/components/keyboard.tsx`
* `sites/promo/src/components/logo-cluster.tsx`
* `sites/promo/src/components/logo-timeline.tsx`
* `sites/promo/src/components/logo.tsx`
* `sites/promo/src/components/map.tsx`
* `sites/promo/src/components/navbar.tsx`
* `sites/promo/src/components/news.tsx`
* `sites/promo/src/components/plus-grid.tsx`
* `sites/promo/src/components/screenshot.tsx`
* `sites/promo/src/components/sponsors.tsx`
* `sites/promo/src/components/team.tsx`

These file types are not supported:
* `.github/dependabot.yml`
* `.github/workflows/ci.yml`
* `.github/workflows/client-release.yml`
* `.github/workflows/codeql.yml`
* `.github/workflows/e2e.yml`
* `.github/workflows/editorconfig-ci.yml`
* `.github/workflows/osv-scanner.yml`
* `.github/workflows/pages.yml`
* `.github/workflows/server-ci.yml`
* `Dockerfile`
* `desktop/main/components/DependencyRequiredModal.vue`
* `desktop/main/components/HeaderProtonSupportWidget.vue`
* `desktop/main/components/HeaderUserWidget.vue`
* `desktop/main/components/InitiateAuthModule.vue`
* `desktop/main/components/LibrarySearch.vue`
* `desktop/main/components/OfflineHeaderWidget.vue`
* `desktop/main/pages/auth/processing.vue`
* `desktop/main/pages/library/[id]/index.vue`
* `desktop/main/pages/queue.vue`
* `desktop/main/pages/setup/server.vue`
* `desktop/main/pages/store/index.vue`
* `libraries/base/components/LoadingButton.vue`
* `libraries/base/components/TextInputModal.vue`
* `libraries/libarchive/Cargo.toml`
* `libraries/native_model/.github/workflows/build_and_test_release.yml`
* `libraries/native_model/.github/workflows/clippy_check.yml`
* `libraries/native_model/.github/workflows/conventional_commits.yml`
* `libraries/native_model/.github/workflows/fmt_check.yml`
* `libraries/native_model/Cargo.toml`
* `libraries/native_model/native_model_macro/Cargo.toml`
* `libraries/native_model/tests_crate/Cargo.toml`
* `server/components/Auth/Simple.vue`
* `server/components/CodeInput.vue`
* `server/components/Directory/Library.vue`
* `server/components/Directory/News.vue`
* `server/components/EmulatorWidget.vue`
* `server/components/GameEditor/Metadata.vue`
* `server/components/GameEditor/Version.vue`
* `server/components/GamePanel.vue`
* `server/components/GameSearchResultWidget.vue`
* `server/components/ImageUpload.vue`
* `server/components/ImportVersionLaunchRow.vue`
* `server/components/Modal/CreateCollection.vue`
* `server/components/Modal/CreateTag.vue`
* `server/components/Modal/CreateToken.vue`
* `server/components/Modal/DeleteCollection.vue`
* `server/components/Modal/DeleteNews.vue`
* `server/components/Modal/DeleteUser.vue`
* `server/components/Modal/SelectLaunch.vue`
* `server/components/NewsArticleCreateButton.vue`
* `server/components/PieChart/PieChart.vue`
* `server/components/Selector/FileExtension.vue`
* `server/components/Selector/MultiItem.vue`
* `server/components/Selector/Platform.vue`
* `server/components/SourceOptions/Filesystem.vue`
* `server/components/SourceOptions/FlatFilesystem.vue`
* `server/components/StoreView.vue`
* `server/components/UserFooter.vue`
* `server/components/UserHeader/StoreNav.vue`
* `server/components/UserHeader/UserWidget.vue`
* `server/error.vue`
* `server/layouts/admin.vue`
* `server/pages/account/index.vue`
* `server/pages/account/security.vue`
* `server/pages/admin/index.vue`
* `server/pages/admin/library/[id]/import.vue`
* `server/pages/admin/library/import.vue`
* `server/pages/admin/library/index.vue`
* `server/pages/admin/library/mass-import.vue`
* `server/pages/admin/library/sources/index.vue`
* `server/pages/admin/metadata/companies/[id]/index.vue`
* `server/pages/admin/metadata/companies/index.vue`
* `server/pages/admin/settings/index.vue`
* `server/pages/admin/task/[id]/index.vue`
* `server/pages/admin/task/index.vue`
* `server/pages/admin/users/auth/index.vue`
* `server/pages/admin/users/auth/simple/index.vue`
* `server/pages/admin/users/index.vue`
* `server/pages/auth/mfa/index.vue`
* `server/pages/auth/register.vue`
* `server/pages/client/authorize/[id].vue`
* `server/pages/library.vue`
* `server/pages/library/game/[id]/index.vue`
* `server/pages/setup.vue`
* `server/pages/store/[id]/index.vue`
* `server/pages/user/[id]/index.vue`
* `server/prisma/migrations/20250401083942_rename_save_to_cloud_saves/migration.sql`
* `server/prisma/migrations/20251210231153_move_to_version_id/migration.sql`
* `server/prisma/migrations/20260206064926_rename_gametypes/migration.sql`
* `sites/docs/.github/workflows/deploy.yml`

* fix: revert S6598 type alias to interface in request.ts (S6598 false positive), fix no-useless-assignment in manifest, fix no-empty catch in torrential

* style: prettier reformatting in promo site

* fix: add explicit postinstall step after pnpm install --ignore-scripts (generate Nuxt and Prisma artifacts)

* fix: CodeRabbit review feedback - ACL security, CodeQL credentials, Rust toolchain, sponsors copy, Vue a11y, swift OSStatus, editorconfig pnpm

* style: rename TODO/FIXME/HACK comments to PENDING (clear SonarCloud S1135)

* style: prettier formatting on touched files

* fix: pre-existing typecheck + prettier issues blocking CI

- server/components/PieChart/utils.ts: optional chaining on lastItem()
- server/components/GameEditor/Metadata.vue: remove orphan </div> tags (2 spots)
- server/.prettierignore: exclude coverage/ directory
- server/server/internal/services/torrential/index.ts: sonarcloud-disable for S4036 (cargoPath is dynamically resolved but env is restricted)

---------

Co-authored-by: John Smith <you@example.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-07-25 12:14:06 -04:00
..
2023-09-02 13:12:23 +02:00

Native model

Crates.io Build Test Release Documentation License

Add interoperability on the top of serialization formats like bincode, postcard etc.

See concepts for more details.

Goals

  • Interoperability: Allows different applications to work together, even if they are using different versions of the data model.
  • Data Consistency: Ensure that we process the data expected model.
  • Flexibility: You can use any serialization format you want. More details here.
  • Performance: A minimal overhead (encode: ~20 ns, decode: ~40 ps). More details here.

Usage

       Application 1 (DotV1)        Application 2 (DotV1 and DotV2)
                |                                  |
   Encode DotV1 |--------------------------------> | Decode DotV1 to DotV2
                |                                  | Modify DotV2
   Decode DotV1 | <--------------------------------| Encode DotV2 back to DotV1
                |                                  |
use native_model::native_model;
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, PartialEq, Debug)]
#[native_model(id = 1, version = 1)]
struct DotV1(u32, u32);

#[derive(Deserialize, Serialize, PartialEq, Debug)]
#[native_model(id = 1, version = 2, from = DotV1)]
struct DotV2 {
    name: String,
    x: u64,
    y: u64,
}

impl From<DotV1> for DotV2 {
    fn from(dot: DotV1) -> Self {
        DotV2 {
            name: "".to_string(),
            x: dot.0 as u64,
            y: dot.1 as u64,
        }
    }
}

impl From<DotV2> for DotV1 {
    fn from(dot: DotV2) -> Self {
        DotV1(dot.x as u32, dot.y as u32)
    }
}

// Application 1
let dot = DotV1(1, 2);
let bytes = native_model::encode(&dot).unwrap();

// Application 1 sends bytes to Application 2.

// Application 2
// We are able to decode the bytes directly into a new type DotV2 (upgrade).
let (mut dot, source_version) = native_model::decode::<DotV2>(bytes).unwrap();
assert_eq!(dot, DotV2 {
    name: "".to_string(),
    x: 1,
    y: 2
});
dot.name = "Dot".to_string();
dot.x = 5;
// For interoperability, we encode the data with the version compatible with Application 1 (downgrade).
let bytes = native_model::encode_downgrade(dot, source_version).unwrap();

// Application 2 sends bytes to Application 1.

// Application 1
let (dot, _) = native_model::decode::<DotV1>(bytes).unwrap();
assert_eq!(dot, DotV1(5, 2));
  • Full example here.

Serialization format

You can use default serialization formats via the feature flags, like:

[dependencies]
native_model = { version = "0.1", features = ["bincode_2"] }

Each feature flag corresponds to a specific minor version of the serialization format. In order to avoid breaking changes, the default serialization format is the oldest one.

Custom serialization format

Define a struct with the name you want. This struct must implement native_model::Encode and native_model::Decode traits.

Full examples:

Others examples, see the default implementations:

Notice

native_model provides implementations that rely on metadata-less formats and serde. There are known issues with some serde advanced features such as:

  • #[serde(flatten)]
  • #[serde(skip)]
  • #[serde(skip_deserializing)]
  • #[serde(skip_serializing)]
  • #[serde(skip_serializing_if = "path")]
  • #[serde(tag = "...")]
  • #[serde(untagged)]

Or types implementing similar strategies such as serde_json::Value.

The rmp-serde serialization format can optionally support them serializing structs as maps, the RmpSerdeNamed struct is provided to support this use-case.

Data model

Define your model using the macro native_model.

Attributes:

  • id = u32: The unique identifier of the model.
  • version = u32: The version of the model.
  • with = type: The serialization format that you use for the Encode/Decode implementation. Setup here.
  • from = type: Optional, the previous version of the model.
    • type: The previous version of the model that you use for the From implementation.
  • try_from = (type, error): Optional, the previous version of the model with error handling.
    • type: The previous version of the model that you use for the TryFrom implementation.
    • error: The error type that you use for the TryFrom implementation.
use native_model::native_model;
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, PartialEq, Debug)]
#[native_model(id = 1, version = 1)]
struct DotV1(u32, u32);

#[derive(Deserialize, Serialize, PartialEq, Debug)]
#[native_model(id = 1, version = 2, from = DotV1)]
struct DotV2 {
    name: String,
    x: u64,
    y: u64,
}

// Implement the conversion between versions From<DotV1> for DotV2 and From<DotV2> for DotV1.

impl From<DotV1> for DotV2 {
    fn from(dot: DotV1) -> Self {
        DotV2 {
            name: "".to_string(),
            x: dot.0 as u64,
            y: dot.1 as u64,
        }
    }
}

impl From<DotV2> for DotV1 {
    fn from(dot: DotV2) -> Self {
        DotV1(dot.x as u32, dot.y as u32)
    }
}

#[derive(Deserialize, Serialize, PartialEq, Debug)]
#[native_model(id = 1, version = 3, try_from = (DotV2, anyhow::Error))]
struct DotV3 {
    name: String,
    cord: Cord,
}

#[derive(Deserialize, Serialize, PartialEq, Debug)]
struct Cord {
    x: u64,
    y: u64,
}

// Implement the conversion between versions From<DotV2> for DotV3 and From<DotV3> for DotV2.

impl TryFrom<DotV2> for DotV3 {
    type Error = anyhow::Error;

    fn try_from(dot: DotV2) -> Result<Self, Self::Error> {
        Ok(DotV3 {
            name: dot.name,
            cord: Cord { x: dot.x, y: dot.y },
        })
    }
}

impl TryFrom<DotV3> for DotV2 {
    type Error = anyhow::Error;

    fn try_from(dot: DotV3) -> Result<Self, Self::Error> {
        Ok(DotV2 {
            name: dot.name,
            x: dot.cord.x,
            y: dot.cord.y,
        })
    }
}

Codecs

native_model comes with several optional built-in serializer features available:

  • bincode 1.3

    • This is the default codec.
    • Warning: This codec may not work with all serde-derived types.
  • bincode 2.0.0-rc.3

    • Enable the bincode_2 feature and use the native_model::bincode_2::Bincode attribute to have native_db use this crate for serializing & deserializing.
    • Warning: This codec may not work with all serde-derived types.
  • postcard 1.0

    • Enable the postcard_1_0 feature and use the native_model::postcard_1_0::PostCard attribute.
    • Warning: This codec may not work with all serde-derived types.
  • rmp-serde 1.3

    • Enable the rmp_serde_1_3 feature and use the native_model::rmp_serde_1_3::RmpSerde attribute.
Codec example:

As example, to use rmp-serde:

  1. In your project's Cargo.toml file, enable the rmp_serde_1_3 feature for the native_model dependency.
    • Be sure to check crates.io for the most recent native_model version number.
[dependencies]
serde = { version = "1.0", features = [ "derive" ] }
native_model = { version = "0.4", features = [ "rmp_serde_1_3" ] }
  1. Assign the rmp_serde_1_3 codec to your struct using the with attribute:
use native_model::native_model;

#[derive(Clone, Default, serde::Deserialize, serde::Serialize)]
#[native_model(id = 1, version = 1, with = native_model::rmp_serde_1_3::RmpSerde)]
struct MyStruct {
	my_string: String,
	// etc.
}
Additional reading

You may also want to check out David Koloski's Rust serialization benchmarks for help selecting the codec (i.e. bincode_1_3, rmp_serde_1_3, etc.) that's best for your project.

Status

Early development. Not ready for production.

Concepts

In order to understand how the native model works, you need to understand the following concepts.

  • Identity(id): The identity is the unique identifier of the model. It is used to identify the model and prevent to decode a model into the wrong Rust type.
  • Version(version) The version is the version of the model. It is used to check the compatibility between two models.
  • Encode: The encode is the process of converting a model into a byte array.
  • Decode: The decode is the process of converting a byte array into a model.
  • Downgrade: The downgrade is the process of converting a model into a previous version of the model.
  • Upgrade: The upgrade is the process of converting a model into a newer version of the model.

Under the hood, the native model is a thin wrapper around serialized data. The id and the version are twice encoded with a little_endian::U32. That represents 8 bytes, that are added at the beginning of the data.

+------------------+------------------+------------------------------------+
|     ID (4 bytes) | Version (4 bytes)| Data (indeterminate-length bytes)  |
+------------------+------------------+------------------------------------+

Full example here.

Performance

Native model has been designed to have a minimal and constant overhead. That means that the overhead is the same whatever the size of the data. Under the hood we use the zerocopy crate to avoid unnecessary copies.

👉 To know the total time of the encode/decode, you need to add the time of your serialization format.

Resume:

  • Encode: ~20 ns
  • Decode: ~40 ps
data size encode time (ns) decode time (ps)
1 B 19.769 ns - 20.154 ns 40.526 ps - 40.617 ps
1 KiB 19.597 ns - 19.971 ns 40.534 ps - 40.633 ps
1 MiB 19.662 ns - 19.910 ns 40.508 ps - 40.632 ps
10 MiB 19.591 ns - 19.980 ns 40.504 ps - 40.605 ps
100 MiB 19.669 ns - 19.867 ns 40.520 ps - 40.644 ps

Benchmark of the native model overhead here.