use chrono::{DateTime, NaiveDateTime, Utc};
use regex::Regex;
use super::{push_bounded, MAX_NAMESERVERS, MAX_STATUSES};
use crate::whois::parser::WhoisResponse;
static_regex! {
NS_PATTERN = r"(?m)^(?:p\.[ \t]+\[ネームサーバ\]|\[(?:Name Server|ネームサーバ)\])[ \t]+(.+)$";
ORG_PATTERN = r"(?m)^g\.[ \t]+\[Organization\][ \t]+(.+)$";
ORG_JP_PATTERN = r"(?m)^f\.[ \t]+\[組織名\][ \t]+(.+)$";
STATUS_PATTERN = r"(?m)^\[(?:状態|Status|State)\][ \t]+(.+)$";
UPDATED_PATTERN = r"(?m)^\[(?:最終更新|Last Updated?)\][ \t]+(.+)$";
CREATED_PATTERN =
r"(?m)^\[(?:登録年月日|接続年月日|Created on|Registered Date|Connected Date)\][ \t]+(.+)$";
EXPIRES_PATTERN = r"(?m)^\[(?:有効期限|Expires on)\][ \t]+(.+)$";
SIGNING_KEY_LINE = r"(?m)^(?:s\.[ \t]+)?\[(?:Signing Key|署名鍵)\][^\S\n]*(.+)?$";
STATE_EXPIRY_PATTERN = r"\((\d{4}/\d{1,2}/\d{1,2})\)";
REGISTRANT_EN_PATTERN = r"(?m)^\[Registrant\][ \t]+(.+)$";
REGISTRANT_JP_PATTERN = r"(?m)^\[登録者名\][ \t]+(.+)$";
NS_EN_PATTERN = r"(?im)^(?:Name Server[ \t]*:?|p\.[ \t]*(?:\[[^\]]*\])?)[ \t]+(.+)$";
}
pub(super) const TLDS: &[&str] = &["jp"];
pub(super) fn parse(domain: &str, server: &str, raw: &str) -> WhoisResponse {
let mut nameservers = Vec::new();
let mut organization = None;
let mut status = Vec::new();
let mut has_signing_key = false;
for caps in NS_PATTERN.captures_iter(raw) {
if let Some(m) = caps.get(1) {
let ns = m.as_str().trim().to_lowercase();
push_bounded(&mut nameservers, ns, MAX_NAMESERVERS);
}
}
if nameservers.is_empty() {
for caps in NS_EN_PATTERN.captures_iter(raw) {
if let Some(m) = caps.get(1) {
let ns = m.as_str().trim().to_lowercase();
push_bounded(&mut nameservers, ns, MAX_NAMESERVERS);
}
}
}
if let Some(caps) = ORG_PATTERN.captures(raw) {
if let Some(m) = caps.get(1) {
let org = m.as_str().trim().to_string();
if !org.is_empty() {
organization = Some(org);
}
}
}
if organization.is_none() {
if let Some(caps) = ORG_JP_PATTERN.captures(raw) {
if let Some(m) = caps.get(1) {
let org = m.as_str().trim().to_string();
if !org.is_empty() {
organization = Some(org);
}
}
}
}
for pattern in [®ISTRANT_EN_PATTERN, ®ISTRANT_JP_PATTERN] {
if organization.is_some() {
break;
}
if let Some(caps) = pattern.captures(raw) {
if let Some(m) = caps.get(1) {
let org = m.as_str().trim().to_string();
if !org.is_empty() {
organization = Some(org);
}
}
}
}
if let Some(caps) = STATUS_PATTERN.captures(raw) {
if let Some(m) = caps.get(1) {
let s = m.as_str().trim().to_string();
push_bounded(&mut status, s, MAX_STATUSES);
}
}
let first_date = |pattern: &Regex| {
pattern
.captures_iter(raw)
.filter_map(|caps| caps.get(1))
.find_map(|m| parse_jprs_date(m.as_str()))
};
let updated_date = first_date(&UPDATED_PATTERN);
let creation_date = first_date(&CREATED_PATTERN);
let expiration_date = first_date(&EXPIRES_PATTERN).or_else(|| {
status.iter().find_map(|s| {
STATE_EXPIRY_PATTERN
.captures(s)
.and_then(|caps| caps.get(1))
.and_then(|m| parse_jprs_date(m.as_str()))
})
});
if let Some(caps) = SIGNING_KEY_LINE.captures(raw) {
if let Some(m) = caps.get(1) {
if !m.as_str().trim().is_empty() {
has_signing_key = true;
}
}
}
let has_data = organization.is_some()
|| !nameservers.is_empty()
|| creation_date.is_some()
|| expiration_date.is_some()
|| !status.is_empty();
WhoisResponse {
domain: domain.to_string(),
registrar: has_data.then(|| "JPRS".to_string()),
registrant: organization.clone(),
organization,
registrant_country: has_data.then(|| "JP".to_string()),
creation_date,
expiration_date,
updated_date,
nameservers,
status,
dnssec: if has_signing_key {
Some("signedDelegation".to_string())
} else {
None
},
whois_server: server.to_string(),
raw_response: raw.to_string(),
..Default::default()
}
}
fn parse_jprs_date(date_str: &str) -> Option<DateTime<Utc>> {
let cleaned = date_str.trim().trim_end_matches("(JST)").trim();
if let Ok(dt) = NaiveDateTime::parse_from_str(cleaned, "%Y/%m/%d %H:%M:%S") {
return dt.and_utc().checked_sub_signed(chrono::Duration::hours(9));
}
if let Ok(dt) =
NaiveDateTime::parse_from_str(&format!("{} 00:00:00", cleaned), "%Y/%m/%d %H:%M:%S")
{
return Some(dt.and_utc());
}
None
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_JPRS_RESPONSE: &str = r#"[ JPRS database provides information on network administration. Its use is ]
[ restricted to network administration purposes. For further information, ]
[ use 'whois -h whois.jprs.jp help'. To suppress Japanese output, add'/e' ]
[ at the end of command, e.g. 'whois -h whois.jprs.jp xxx/e'. ]
Domain Information: [ドメイン情報]
a. [ドメイン名] TOKYO.JP
e. [そしきめい]
f. [組織名] 東京ドメイン
g. [Organization] Tokyo Domain
k. [組織種別]
l. [Organization Type]
m. [登録担当者]
n. [技術連絡担当者]
p. [ネームサーバ] a.dns.jp
p. [ネームサーバ] b.dns.jp
p. [ネームサーバ] c.dns.jp
p. [ネームサーバ] d.dns.jp
p. [ネームサーバ] e.dns.jp
p. [ネームサーバ] f.dns.jp
p. [ネームサーバ] g.dns.jp
p. [ネームサーバ] h.dns.jp
s. [署名鍵]
[状態] Reserved
[登録年月日]
[接続年月日]
[最終更新] 2005/03/30 17:37:52 (JST)"#;
const SAMPLE_JPRS_DOMAIN: &str = r#"[ JPRS database provides information on network administration. Its use is ]
Domain Information: [ドメイン情報]
a. [ドメイン名] EXAMPLE.JP
f. [組織名] 株式会社エグザンプル
g. [Organization] Example Inc.
p. [ネームサーバ] ns1.example.jp
p. [ネームサーバ] ns2.example.jp
s. [署名鍵]
[状態] Active
[登録年月日] 2001/03/22
[接続年月日] 2001/03/22
[最終更新] 2024/04/01 01:05:10 (JST)"#;
#[test]
fn test_jprs_nameservers() {
let result = parse("tokyo.jp", "whois.jprs.jp", SAMPLE_JPRS_RESPONSE);
assert_eq!(result.nameservers.len(), 8);
assert!(result.nameservers.contains(&"a.dns.jp".to_string()));
assert!(result.nameservers.contains(&"h.dns.jp".to_string()));
}
#[test]
fn test_jprs_organization() {
let result = parse("tokyo.jp", "whois.jprs.jp", SAMPLE_JPRS_RESPONSE);
assert_eq!(result.organization, Some("Tokyo Domain".to_string()));
}
#[test]
fn test_jprs_status() {
let result = parse("tokyo.jp", "whois.jprs.jp", SAMPLE_JPRS_RESPONSE);
assert!(result.status.contains(&"Reserved".to_string()));
}
#[test]
fn test_jprs_updated_date() {
let result = parse("tokyo.jp", "whois.jprs.jp", SAMPLE_JPRS_RESPONSE);
assert!(result.updated_date.is_some());
}
#[test]
fn test_jprs_domain_with_dates() {
let result = parse("example.jp", "whois.jprs.jp", SAMPLE_JPRS_DOMAIN);
assert_eq!(result.nameservers.len(), 2);
assert!(result.nameservers.contains(&"ns1.example.jp".to_string()));
assert!(result.nameservers.contains(&"ns2.example.jp".to_string()));
assert_eq!(result.organization, Some("Example Inc.".to_string()));
assert!(result.status.contains(&"Active".to_string()));
assert!(result.creation_date.is_some());
assert!(result.updated_date.is_some());
}
#[test]
fn test_jprs_no_signing_key() {
let result = parse("tokyo.jp", "whois.jprs.jp", SAMPLE_JPRS_RESPONSE);
assert!(result.dnssec.is_none());
}
const SAMPLE_JPRS_EN_BRACKET_NS: &str = r#"Domain Information:
a. [Domain Name] EXAMPLE.JP
p. [Name Server] a.dns.jp
p. [Name Server] b.dns.jp
[State] Active"#;
#[test]
fn test_jprs_en_fallback_strips_bracket_label() {
let result = parse("example.jp", "whois.jprs.jp", SAMPLE_JPRS_EN_BRACKET_NS);
assert_eq!(
result.nameservers,
vec!["a.dns.jp".to_string(), "b.dns.jp".to_string()],
"fallback must capture only the hostname, not the bracket label"
);
}
const SAMPLE_JPRS_CURRENT: &str = r#"[ JPRS database provides information on network administration. Its use is ]
Domain Information: [ドメイン情報]
[Domain Name] JPRS.JP
[登録者名] 株式会社日本レジストリサービス
[Registrant] Japan Registry Services Co.,Ltd.
[Name Server] ns1.jprs.jp
[Name Server] ns2.jprs.jp
[Name Server] ns3.jprs.jp
[Name Server] ns4.jprs.jp
[Signing Key] 7240 8 2 (
E147A85589E24FE0DBB5980C73501B5D
D656BE5550714F150BE574AE8777B77D )
[登録年月日] 2001/02/02
[有効期限] 2027/02/28
[状態] Active
[最終更新] 2026/03/01 01:05:03 (JST)"#;
const SAMPLE_JPRS_CURRENT_EN: &str = r#"Domain Information:
[Domain Name] JPRS.JP
[Registrant] Japan Registry Services Co.,Ltd.
[Name Server] ns1.jprs.jp
[Name Server] ns2.jprs.jp
[Signing Key] 7240 8 2 (
E147A85589E24FE0DBB5980C73501B5D )
[Created on] 2001/02/02
[Expires on] 2027/02/28
[Status] Active
[Last Updated] 2026/03/01 01:05:03 (JST)"#;
#[test]
fn test_jprs_current_format_nameservers_and_dates() {
let result = parse("jprs.jp", "whois.jprs.jp", SAMPLE_JPRS_CURRENT);
assert_eq!(
result.nameservers,
vec!["ns1.jprs.jp", "ns2.jprs.jp", "ns3.jprs.jp", "ns4.jprs.jp"]
);
assert!(result.creation_date.is_some(), "created from [登録年月日]");
assert!(result.expiration_date.is_some(), "expiry from [有効期限]");
assert!(result.status.contains(&"Active".to_string()));
assert_eq!(
result.organization.as_deref(),
Some("Japan Registry Services Co.,Ltd.")
);
assert_eq!(result.dnssec.as_deref(), Some("signedDelegation"));
}
#[test]
fn test_jprs_no_match_is_available() {
let raw = "[ JPRS database provides information on network administration. Its use is ]\n\
[ restricted to network administration purposes. For further information, ]\n\
No match!!\n";
let result = parse("nosuchdomain.jp", "whois.jprs.jp", raw);
assert!(
result.registrar.is_none(),
"no-match must not synthesize a registrar"
);
assert!(
result.is_available(),
"No match!! must mark the domain available"
);
}
#[test]
fn test_jprs_current_format_english_labels() {
let result = parse("jprs.jp", "whois.jprs.jp", SAMPLE_JPRS_CURRENT_EN);
assert_eq!(result.nameservers, vec!["ns1.jprs.jp", "ns2.jprs.jp"]);
assert!(result.creation_date.is_some(), "created from [Created on]");
assert!(result.expiration_date.is_some(), "expiry from [Expires on]");
assert!(result.status.contains(&"Active".to_string()));
assert_eq!(result.dnssec.as_deref(), Some("signedDelegation"));
}
#[test]
fn test_jprs_empty_fields_do_not_capture_next_line() {
let raw = "Domain Information:\n\
a. [Domain Name] EXAMPLE.CO.JP\n\
g. [Organization] \n\
l. [Organization Type] Company\n\
p. [Name Server] \n\
s. [Signing Key] \n\
[State] Connected (2027/03/31)\n";
let result = parse("example.co.jp", "whois.jprs.jp", raw);
assert!(
result.nameservers.is_empty(),
"empty NS line must not yield the next line: {:?}",
result.nameservers
);
assert_eq!(
result.organization, None,
"empty [Organization] must not capture [Organization Type]"
);
}
const SAMPLE_JPRS_CO_JP_EN: &str = "\
[ JPRS database provides information on network administration. Its use is ]
[ restricted to network administration purposes. For further information, ]
[ use 'whois -h whois.jprs.jp help'. To suppress Japanese output, add'/e' ]
[ at the end of command, e.g. 'whois -h whois.jprs.jp xxx/e'. ]
Domain Information:
a. [Domain Name] GOOGLE.CO.JP
g. [Organization] Google Japan G.K.
l. [Organization Type] Limited Liability Company
m. [Administrative Contact] DL152JP
n. [Technical Contact] TW124137JP
p. [Name Server] ns1.google.com
p. [Name Server] ns2.google.com
p. [Name Server] ns3.google.com
p. [Name Server] ns4.google.com
s. [Signing Key] \n\
[State] Connected (2027/03/31)
[Lock Status] DomainTransferLocked
[Lock Status] AgentChangeLocked
[Registered Date] 2001/03/22
[Connected Date] 2001/03/22
[Last Update] 2026/04/01 01:05:10 (JST)
";
#[test]
fn test_jprs_attribute_type_co_jp_labels() {
use chrono::{Datelike, Timelike};
let result = parse("google.co.jp", "whois.jprs.jp", SAMPLE_JPRS_CO_JP_EN);
assert_eq!(
result.nameservers,
vec![
"ns1.google.com",
"ns2.google.com",
"ns3.google.com",
"ns4.google.com"
]
);
assert_eq!(result.organization.as_deref(), Some("Google Japan G.K."));
let created = result
.creation_date
.expect("created from [Registered Date]");
assert_eq!(
(created.year(), created.month(), created.day()),
(2001, 3, 22)
);
let updated = result.updated_date.expect("updated from [Last Update]");
assert_eq!(
(updated.month(), updated.day(), updated.hour()),
(3, 31, 16),
"JST must be converted to UTC"
);
let expires = result
.expiration_date
.expect("expiry from the Connected (YYYY/MM/DD) state");
assert_eq!(
(expires.year(), expires.month(), expires.day()),
(2027, 3, 31)
);
assert_eq!(
result.dnssec, None,
"an empty s. [Signing Key] means unsigned"
);
let signed = SAMPLE_JPRS_CO_JP_EN.replace(
"s. [Signing Key] \n",
"s. [Signing Key] 12345 8 2 ABCDEF0123456789\n",
);
assert_ne!(signed, SAMPLE_JPRS_CO_JP_EN, "fixture replacement applied");
let result = parse("google.co.jp", "whois.jprs.jp", &signed);
assert_eq!(result.dnssec.as_deref(), Some("signedDelegation"));
}
#[test]
fn test_jprs_date_at_chrono_min_does_not_panic() {
let min = NaiveDateTime::MIN.format("%Y/%m/%d %H:%M:%S").to_string();
assert!(
NaiveDateTime::parse_from_str(&min, "%Y/%m/%d %H:%M:%S").is_ok(),
"fixture must reach the JST-offset subtraction: {min}"
);
assert_eq!(parse_jprs_date(&format!("{min} (JST)")), None);
let raw = format!(
"Domain Information:\n\
[Domain Name] EXAMPLE.JP\n\
[Name Server] ns1.example.jp\n\
[Last Updated] {min} (JST)\n"
);
let result = parse("example.jp", "whois.jprs.jp", &raw);
assert_eq!(result.updated_date, None);
assert_eq!(result.nameservers, vec!["ns1.example.jp"]);
}
}