mirror of https://github.com/synctv-org/synctv
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
356 lines
11 KiB
Rust
356 lines
11 KiB
Rust
use std::{fs, io, path::Path};
|
|
|
|
use prost::Message;
|
|
use prost_types::{DescriptorProto, FileDescriptorSet};
|
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
println!("cargo:rerun-if-changed=proto/management.proto");
|
|
|
|
let out_dir = std::env::var("OUT_DIR")?;
|
|
let out_dir = Path::new(&out_dir);
|
|
let prost_config = prost_config();
|
|
tonic_prost_build::configure()
|
|
.build_server(true)
|
|
.build_client(true)
|
|
.file_descriptor_set_path(out_dir.join("descriptor.bin"))
|
|
.out_dir(out_dir)
|
|
.compile_with_config(
|
|
prost_config,
|
|
&["proto/management.proto"],
|
|
&[".", "..", "../synctv-proto"],
|
|
)?;
|
|
build_pbjson(out_dir)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn prost_config() -> tonic_prost_build::Config {
|
|
let mut config = tonic_prost_build::Config::new();
|
|
config.extern_path(".synctv.admin", "::synctv_proto::admin");
|
|
config.extern_path(".synctv.client", "::synctv_proto::client");
|
|
config.extern_path(".synctv.common", "::synctv_proto::common");
|
|
config.extern_path(".synctv.source_config", "::synctv_proto::source_config");
|
|
config.extern_path(".synctv.provider.alist", "::synctv_proto::providers::alist");
|
|
config.extern_path(
|
|
".synctv.provider.bilibili",
|
|
"::synctv_proto::providers::bilibili",
|
|
);
|
|
config.extern_path(
|
|
".synctv.provider.common",
|
|
"::synctv_proto::providers::common",
|
|
);
|
|
config.extern_path(
|
|
".synctv.provider.douyin",
|
|
"::synctv_proto::providers::douyin",
|
|
);
|
|
config.extern_path(
|
|
".synctv.provider.tiktok",
|
|
"::synctv_proto::providers::tiktok",
|
|
);
|
|
config.extern_path(
|
|
".synctv.provider.twitch",
|
|
"::synctv_proto::providers::twitch",
|
|
);
|
|
config.extern_path(".synctv.provider.emby", "::synctv_proto::providers::emby");
|
|
config.extern_path(".synctv.provider.rtmp", "::synctv_proto::providers::rtmp");
|
|
for provider in [
|
|
"acfun",
|
|
"cctv",
|
|
"cloudreve",
|
|
"douyu",
|
|
"fnos",
|
|
"huya",
|
|
"nextcloud",
|
|
"qnap",
|
|
"seafile",
|
|
"synology",
|
|
"truenas",
|
|
"youtube",
|
|
] {
|
|
config.extern_path(
|
|
format!(".synctv.provider.{provider}"),
|
|
format!("::synctv_proto::providers::{provider}"),
|
|
);
|
|
}
|
|
config
|
|
}
|
|
|
|
fn proto_snake_to_lower_camel(value: &str) -> String {
|
|
let mut output = String::with_capacity(value.len());
|
|
let mut uppercase_next = false;
|
|
for character in value.chars() {
|
|
if character == '_' {
|
|
uppercase_next = true;
|
|
} else if uppercase_next {
|
|
output.extend(character.to_uppercase());
|
|
uppercase_next = false;
|
|
} else {
|
|
output.push(character);
|
|
}
|
|
}
|
|
output
|
|
}
|
|
|
|
fn collect_message_json_name_aliases(
|
|
message: DescriptorProto,
|
|
aliases: &mut Vec<(String, String)>,
|
|
) {
|
|
for field in message.field {
|
|
let Some(proto_name) = field.name else {
|
|
continue;
|
|
};
|
|
let json_name = field
|
|
.json_name
|
|
.unwrap_or_else(|| proto_snake_to_lower_camel(&proto_name));
|
|
if proto_name != json_name {
|
|
aliases.push((proto_name, json_name));
|
|
}
|
|
}
|
|
for nested_message in message.nested_type {
|
|
collect_message_json_name_aliases(nested_message, aliases);
|
|
}
|
|
}
|
|
|
|
fn collect_json_name_aliases(out_dir: &Path) -> io::Result<Vec<(String, String)>> {
|
|
let descriptor_bytes = fs::read(out_dir.join("descriptor.bin"))?;
|
|
let descriptor_set = FileDescriptorSet::decode(descriptor_bytes.as_slice())
|
|
.map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))?;
|
|
let mut aliases = Vec::new();
|
|
for file in descriptor_set.file {
|
|
for message in file.message_type {
|
|
collect_message_json_name_aliases(message, &mut aliases);
|
|
}
|
|
}
|
|
Ok(aliases)
|
|
}
|
|
|
|
fn string_array_literal(line: &str) -> Option<&str> {
|
|
let trimmed = line.trim();
|
|
let trimmed = trimmed.strip_suffix(',')?;
|
|
let trimmed = trimmed.strip_prefix('"')?;
|
|
trimmed.strip_suffix('"')
|
|
}
|
|
|
|
fn is_proto_json_name_alias(
|
|
line: &str,
|
|
next_line: Option<&str>,
|
|
json_name_aliases: &[(String, String)],
|
|
) -> bool {
|
|
let Some(value) = string_array_literal(line) else {
|
|
return false;
|
|
};
|
|
let Some(next_value) = next_line.and_then(string_array_literal) else {
|
|
return false;
|
|
};
|
|
json_name_aliases
|
|
.iter()
|
|
.any(|(proto_name, json_name)| value == proto_name && next_value == json_name)
|
|
}
|
|
|
|
fn strict_generated_field_match_alias(line: &str) -> Option<String> {
|
|
let arrow_index = line.find("=> Ok(GeneratedField::")?;
|
|
let (patterns, rest) = line.split_at(arrow_index);
|
|
if !patterns.contains('|') {
|
|
return None;
|
|
}
|
|
|
|
let indent_len = patterns.len() - patterns.trim_start().len();
|
|
let indent = &patterns[..indent_len];
|
|
let mut canonical_patterns = patterns[indent_len..]
|
|
.split('|')
|
|
.map(str::trim)
|
|
.filter(|pattern| !pattern.trim_matches('"').contains('_'));
|
|
let canonical_pattern = canonical_patterns.next()?;
|
|
Some(format!("{indent}{canonical_pattern} {rest}"))
|
|
}
|
|
|
|
fn count_char(line: &str, needle: char) -> i32 {
|
|
i32::try_from(line.matches(needle).count()).unwrap_or(i32::MAX)
|
|
}
|
|
|
|
fn is_enum_string_name(value: &str) -> bool {
|
|
value
|
|
.chars()
|
|
.all(|ch| ch.is_ascii_uppercase() || ch.is_ascii_digit() || ch == '_')
|
|
}
|
|
|
|
fn strict_generated_line(line: &str) -> String {
|
|
line.replace(
|
|
"deserializer.deserialize_any(GeneratedVisitor)",
|
|
"deserializer.deserialize_i64(GeneratedVisitor)",
|
|
)
|
|
.replace(
|
|
"expected one of: {:?}\", &FIELDS",
|
|
"expected one of: {:?}\", FIELDS",
|
|
)
|
|
.replace(
|
|
"write!(formatter, \"expected one of: {:?}\", FIELDS)",
|
|
"formatter.write_str(\"integer enum value\")",
|
|
)
|
|
}
|
|
|
|
fn strict_pbjson_serde_source(source: &str, json_name_aliases: &[(String, String)]) -> String {
|
|
let lines: Vec<&str> = source.lines().collect();
|
|
let mut output = Vec::with_capacity(lines.len());
|
|
let mut index = 0;
|
|
|
|
while index < lines.len() {
|
|
let line = lines[index];
|
|
let next_line = lines.get(index + 1).copied();
|
|
|
|
if is_proto_json_name_alias(line, next_line, json_name_aliases) {
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
if line
|
|
.contains("fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>")
|
|
{
|
|
let mut depth = 0_i32;
|
|
let mut saw_body = false;
|
|
while index < lines.len() {
|
|
let skipped = lines[index];
|
|
depth += count_char(skipped, '{');
|
|
if skipped.contains('{') {
|
|
saw_body = true;
|
|
}
|
|
depth -= count_char(skipped, '}');
|
|
index += 1;
|
|
if saw_body && depth == 0 {
|
|
break;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if line.contains("const FIELDS: &[&str] = &[")
|
|
&& lines
|
|
.get(index + 1)
|
|
.is_some_and(|next| string_array_literal(next).is_some_and(is_enum_string_name))
|
|
{
|
|
while index < lines.len() {
|
|
let skipped = lines[index];
|
|
index += 1;
|
|
if skipped.trim() == "];" {
|
|
break;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if let Some(strict_line) = strict_generated_field_match_alias(line) {
|
|
output.push(strict_line);
|
|
} else {
|
|
output.push(strict_generated_line(line));
|
|
}
|
|
index += 1;
|
|
}
|
|
|
|
let mut strict_source = output.join("\n");
|
|
if source.ends_with('\n') {
|
|
strict_source.push('\n');
|
|
}
|
|
strict_source
|
|
}
|
|
|
|
fn verify_strict_pbjson_serde_source(
|
|
source: &str,
|
|
json_name_aliases: &[(String, String)],
|
|
) -> Result<(), String> {
|
|
for forbidden in [
|
|
"deserializer.deserialize_any(GeneratedVisitor)",
|
|
"expected one of: {:?}\", &FIELDS",
|
|
"write!(formatter, \"expected one of: {:?}\", FIELDS)",
|
|
"fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>",
|
|
] {
|
|
if source.contains(forbidden) {
|
|
return Err(format!(
|
|
"strict ProtoJSON output still contains `{forbidden}`"
|
|
));
|
|
}
|
|
}
|
|
|
|
let lines: Vec<&str> = source.lines().collect();
|
|
for (index, line) in lines.iter().enumerate() {
|
|
if is_proto_json_name_alias(line, lines.get(index + 1).copied(), json_name_aliases) {
|
|
let proto_name = string_array_literal(line).unwrap_or_default();
|
|
return Err(format!(
|
|
"strict ProtoJSON output still advertises proto field alias `{proto_name}`"
|
|
));
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn stricten_pbjson_serde_files(out_dir: &Path) -> io::Result<()> {
|
|
let json_name_aliases = collect_json_name_aliases(out_dir)?;
|
|
for entry in fs::read_dir(out_dir)? {
|
|
let path = entry?.path();
|
|
let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
|
|
continue;
|
|
};
|
|
if !file_name.ends_with(".serde.rs") {
|
|
continue;
|
|
}
|
|
|
|
let source = fs::read_to_string(&path)?;
|
|
let strict_source = strict_pbjson_serde_source(&source, &json_name_aliases);
|
|
if strict_source != source {
|
|
fs::write(&path, &strict_source)?;
|
|
}
|
|
verify_strict_pbjson_serde_source(&strict_source, &json_name_aliases).map_err(|error| {
|
|
io::Error::new(
|
|
io::ErrorKind::InvalidData,
|
|
format!("{}: {error}", path.display()),
|
|
)
|
|
})?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn build_pbjson(out_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
|
let descriptor_set = fs::read(out_dir.join("descriptor.bin"))?;
|
|
let mut builder = pbjson_build::Builder::new();
|
|
builder.out_dir(out_dir).use_integers_for_enums();
|
|
builder.extern_path(".google.protobuf", "::pbjson_types");
|
|
builder.extern_path(".synctv.admin", "::synctv_proto::admin");
|
|
builder.extern_path(".synctv.client", "::synctv_proto::client");
|
|
builder.extern_path(".synctv.common", "::synctv_proto::common");
|
|
builder.extern_path(".synctv.source_config", "::synctv_proto::source_config");
|
|
builder.extern_path(".synctv.provider.alist", "::synctv_proto::providers::alist");
|
|
builder.extern_path(
|
|
".synctv.provider.bilibili",
|
|
"::synctv_proto::providers::bilibili",
|
|
);
|
|
builder.extern_path(
|
|
".synctv.provider.common",
|
|
"::synctv_proto::providers::common",
|
|
);
|
|
builder.extern_path(".synctv.provider.emby", "::synctv_proto::providers::emby");
|
|
builder.extern_path(".synctv.provider.rtmp", "::synctv_proto::providers::rtmp");
|
|
for provider in [
|
|
"acfun",
|
|
"cctv",
|
|
"cloudreve",
|
|
"douyu",
|
|
"fnos",
|
|
"huya",
|
|
"nextcloud",
|
|
"qnap",
|
|
"seafile",
|
|
"synology",
|
|
"truenas",
|
|
"youtube",
|
|
] {
|
|
builder.extern_path(
|
|
format!(".synctv.provider.{provider}"),
|
|
format!("::synctv_proto::providers::{provider}"),
|
|
);
|
|
}
|
|
builder.register_descriptors(&descriptor_set)?;
|
|
builder.build(&[".synctv.management"])?;
|
|
stricten_pbjson_serde_files(out_dir)?;
|
|
Ok(())
|
|
}
|