libsood

Zig library for Roon Core discovery message, with C-compatible API and WebAssembly.

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
  62. 62
  63. 63
  64. 64
  65. 65
  66. 66
  67. 67
  68. 68
  69. 69
  70. 70
  71. 71
  72. 72
  73. 73
  74. 74
  75. 75
  76. 76
  77. 77
  78. 78
  79. 79
  80. 80
  81. 81
  82. 82
  83. 83
  84. 84
  85. 85
  86. 86
  87. 87
  88. 88
  89. 89
  90. 90
  91. 91
  92. 92
  93. 93
  94. 94
  95. 95
  96. 96
  97. 97
  98. 98
  99. 99
  100. 100
  101. 101
  102. 102
  103. 103
  104. 104
  105. 105
  106. 106
  107. 107
  108. 108
  109. 109
  110. 110
  111. 111
  112. 112
  113. 113
  114. 114
  115. 115
  116. 116
  117. 117
  118. 118
  119. 119
  120. 120
  121. 121
  122. 122
  123. 123
  124. 124
  125. 125
  126. 126
// Copyright 2025 Shota FUJI
//
// Licensed under the Zero-Clause BSD License or the Apache License, Version 2.0, at your option.
// You may not use, copy, modify, or distribute this file except according to those terms. You can
// find a copy of the Zero-Clause BSD License at LICENSES/0BSD.txt, and a copy of the Apache License,
// Version 2.0 at LICENSES/Apache-2.0.txt. You may also obtain a copy of the Zero-Clause BSD License
// at <https://opensource.org/license/0bsd> and a copy of the Apache License, Version 2.0 at
// <https://www.apache.org/licenses/LICENSE-2.0>
//
// SPDX-License-Identifier: 0BSD OR Apache-2.0

//! This module contains constants and concret type on top of Message struct.

const std = @import("std");

const constants = @import("constants.zig");
const Message = @import("Message.zig");

/// Target UDP port to use for sending IP multicast and broadcast.
/// Though you can also listen for this UDP port to receive broadcast from another client,
/// this library does not provide API for handling that case.
pub const udp_port = 9003;

/// IPv4 address for IP multicast. Send SOOD message to this IP address on `udp_port`.
pub const multicast_ipv4_address = [4]u8{ 239, 255, 90, 90 };

pub const Query = struct {
    /// A required property for a discovery query. Node.js API uses hard-coded ID
    /// thus this library uses the same one.
    pub const query_service_id = Message.Property{
        .key = constants.query_service_id_key,
        .value = constants.query_service_id_value,
    };

    pub const properties = [_]Message.Property{
        query_service_id,
    };

    /// Premade bytes for minimum discovery query message.
    pub const prebuilt = constants.prebuilt_query;
};

test "Query.prebuilt should equal to one built by `write`" {
    const built = try Message.write(std.testing.allocator, .query, &Query.properties);
    defer std.testing.allocator.free(built);

    try std.testing.expectEqualSlices(u8, built, Query.prebuilt);
}

/// Only subset of fields are stored; everything else is omitted.
/// To access undocumented fields, use `Message.parse` API instead.
pub const Response = struct {
    /// TCP port to use for WebSocket.
    http_port: u16,

    /// Display name of the Roon server.
    ///
    /// This string is what user see in Settings/General page and can be changed at Settings/Setup
    /// page.
    name: []const u8,

    /// Roon server version strings.
    ///
    /// As the name suggests, this value is for display purpose. Format is not defined and there is
    /// no stability guarantee, therefore parsing of this value is not recommended.
    display_version: []const u8,

    /// ID unique to the Roon server.
    unique_id: []const u8,

    pub const SchemaError = error{
        /// Message's type (kind) is not "response" (`R`)
        NonResponseKindMessage,

        /// Response message does not contain required property.
        MissingRequiredProperty,

        /// Cannot convert property value to desired type.
        UnexpectedPropertyValue,
    };

    pub const ParseError = SchemaError || Message.HeaderParseError || Message.PropertyParseError;

    pub fn parse(bytes: []const u8) ParseError!@This() {
        const msg = try Message.parse(bytes);
        if (msg.kind != .response) {
            return SchemaError.NonResponseKindMessage;
        }

        var http_port: ?u16 = null;
        var name: ?[]const u8 = null;
        var display_version: ?[]const u8 = null;
        var unique_id: ?[]const u8 = null;

        var iter = msg.iterator();

        while (try iter.next()) |property| {
            if (std.mem.eql(u8, "http_port", property.key)) {
                http_port = std.fmt.parseInt(u16, property.value, 10) catch return SchemaError.UnexpectedPropertyValue;
                continue;
            }

            if (std.mem.eql(u8, "name", property.key)) {
                name = property.value;
                continue;
            }

            if (std.mem.eql(u8, "display_version", property.key)) {
                display_version = property.value;
                continue;
            }

            if (std.mem.eql(u8, "unique_id", property.key)) {
                unique_id = property.value;
                continue;
            }
        }

        return Response{
            .http_port = http_port orelse return SchemaError.MissingRequiredProperty,
            .name = name orelse return SchemaError.MissingRequiredProperty,
            .display_version = display_version orelse return SchemaError.MissingRequiredProperty,
            .unique_id = unique_id orelse return SchemaError.MissingRequiredProperty,
        };
    }
};