plac

Unofficial Roon clients

  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
// Copyright 2025 Shota FUJI
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

const std = @import("std");

const ExtensionRegistration = struct {
    core_id: []const u8,
    token: []const u8,
};

const FileContent = struct {
    registrations: []ExtensionRegistration,
};

registrations: std.ArrayList(ExtensionRegistration),
allocator: std.mem.Allocator,
file_path: ?[]const u8,

/// Unless user provided custom file path, connection state will be saved to
/// and loaded from this file.
pub const default_path = ".roon.json";

/// Creates a new empty state. Save operation on the returned state is no-op.
pub fn initEphemeral(allocator: std.mem.Allocator) @This() {
    return .{
        .registrations = std.ArrayList(ExtensionRegistration).init(allocator),
        .allocator = allocator,
        .file_path = null,
    };
}

pub const InitOptions = struct {
    file_path: []const u8 = default_path,
};

pub fn init(allocator: std.mem.Allocator, opts: InitOptions) !@This() {
    const file = std.fs.cwd().openFile(opts.file_path, .{}) catch |err| switch (err) {
        error.FileNotFound => {
            return .{
                .registrations = std.ArrayList(ExtensionRegistration).init(allocator),
                .allocator = allocator,
                .file_path = opts.file_path,
            };
        },
        else => return err,
    };
    defer file.close();

    const contents = try file.readToEndAlloc(allocator, std.math.maxInt(usize));
    defer allocator.free(contents);

    const loaded = try std.json.parseFromSlice(FileContent, allocator, contents, .{});
    defer loaded.deinit();

    var registrations = try allocator.alloc(ExtensionRegistration, loaded.value.registrations.len);
    for (loaded.value.registrations, 0..) |r, i| {
        registrations[i] = .{
            .core_id = try allocator.dupe(u8, r.core_id),
            .token = try allocator.dupe(u8, r.token),
        };
    }

    return @This(){
        .registrations = std.ArrayList(ExtensionRegistration).fromOwnedSlice(allocator, registrations),
        .allocator = allocator,
        .file_path = opts.file_path,
    };
}

/// Update or add token. Updated entry copies input buffer: caller can release input buffers
/// then access struct fields.
pub fn putToken(self: *@This(), core_id: []const u8, token: []const u8) std.mem.Allocator.Error!void {
    for (self.registrations.items) |*r| {
        if (std.mem.eql(u8, r.core_id, core_id)) {
            self.allocator.free(r.token);
            r.token = try self.allocator.dupe(u8, token);
            return;
        }
    }

    try self.registrations.append(.{
        .core_id = try self.allocator.dupe(u8, core_id),
        .token = try self.allocator.dupe(u8, token),
    });
}

pub fn save(self: *const @This()) !void {
    const file_path = self.file_path orelse return;

    const file = try std.fs.cwd().createFile(file_path, .{});
    defer file.close();

    try std.json.stringify(
        FileContent{ .registrations = self.registrations.items },
        .{ .whitespace = .indent_tab },
        file.writer(),
    );
}

pub fn deinit(self: *@This()) void {
    for (self.registrations.items) |r| {
        self.allocator.free(r.core_id);
        self.allocator.free(r.token);
    }
    self.registrations.deinit();
}