1 Commits
Author SHA1 Message Date
pandorafuture 7bed9e767a feat: expose avatars and image quality metadata 2026-07-22 11:20:16 +08:00
17 changed files with 247 additions and 34 deletions
+13
View File
@@ -2,6 +2,19 @@
All notable changes to this project will be documented in this file.
## [Unreleased]
## [0.7.4] - 2026-07-22
### Features
- **Contact and conversation avatars** — Expose an optional `avatar_url` in contact and enriched session JSON, preferring the small avatar and falling back to the large avatar while remaining compatible with older database schemas
- **Image quality metadata** — Mark `/api/v1/media` image responses as `full` or `thumbnail`, while continuing to prefer locally available high-resolution/original image files
### Maintenance
- Keep formatting and Clippy clean on the current stable Rust toolchain, and serialize integration-test server startup to avoid ephemeral-port races
## [0.7.3] - 2026-07-10
### Features
+31
View File
@@ -2,6 +2,37 @@
本文件记录项目的所有重要变更。
## [未发布]
## [0.7.4] - 2026-07-22
### 功能
- **联系人和会话头像** — 联系人与会话 JSON 新增可选 `avatar_url`;优先使用小头像、回退大头像,并兼容没有头像字段的旧数据库
- **图片质量元数据** — `/api/v1/media` 图片响应会标记为 `full``thumbnail`,并继续优先返回本机已有的高清/原图文件
### 维护
- 适配当前 stable Rust 的格式化和 Clippy 检查,并串行化集成测试服务启动,避免并行抢占临时端口
## [0.7.3] - 2026-07-10
### 功能
- **Agent 可用的微信数据层** — 提供 Agent Skill,让 Claude Code、Codex、Cursor 等 Agent 查询和订阅本地微信数据
- **跨会话时间线** — 新增 `GET /api/v1/timeline`,可在有界时间范围内一次读取所有会话,并支持分页、排序、消息类型筛选和隐私过滤
- **精简时间线输出** — 返回记忆、归档和报告 Agent 所需的会话、发送者、方向、时间、类型和摘要字段
### 性能
- 复用 SQLCipher 派生密钥,避免在打开、计数、刷新和重开数据库时重复执行 25.6 万轮 KDF
- 限制时间线排序内存,长驻服务会复用预热的数据库连接
### 文档与维护
- 重写 README,补充 Release 安装方式和 Agent Skill 说明
- 更新依赖与 GitHub Actions,恢复格式化、Clippy 和全量测试基线
## [0.7.2] - 2026-04-06
### 功能
Generated
+8 -8
View File
@@ -3132,7 +3132,7 @@ checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
[[package]]
name = "wx-cli"
version = "0.7.3"
version = "0.7.4"
dependencies = [
"axum",
"chrono",
@@ -3168,7 +3168,7 @@ dependencies = [
[[package]]
name = "wx-context"
version = "0.7.3"
version = "0.7.4"
dependencies = [
"aes",
"cbc",
@@ -3192,7 +3192,7 @@ dependencies = [
[[package]]
name = "wx-db"
version = "0.7.3"
version = "0.7.4"
dependencies = [
"hex",
"insta",
@@ -3209,7 +3209,7 @@ dependencies = [
[[package]]
name = "wx-decrypt"
version = "0.7.3"
version = "0.7.4"
dependencies = [
"aes",
"cbc",
@@ -3222,7 +3222,7 @@ dependencies = [
[[package]]
name = "wx-keychain"
version = "0.7.3"
version = "0.7.4"
dependencies = [
"aes",
"cbc",
@@ -3244,7 +3244,7 @@ dependencies = [
[[package]]
name = "wx-media"
version = "0.7.3"
version = "0.7.4"
dependencies = [
"aes",
"base64",
@@ -3262,7 +3262,7 @@ dependencies = [
[[package]]
name = "wx-monitor"
version = "0.7.3"
version = "0.7.4"
dependencies = [
"aes",
"cbc",
@@ -3283,7 +3283,7 @@ dependencies = [
[[package]]
name = "wx-paths"
version = "0.7.3"
version = "0.7.4"
dependencies = [
"dirs",
"libc",
+1 -1
View File
@@ -3,7 +3,7 @@ members = ["crates/wx-decrypt", "crates/wx-keychain", "crates/wx-cli", "crates/w
resolver = "2"
[workspace.package]
version = "0.7.3"
version = "0.7.4"
edition = "2021"
license = "MIT"
repository = "https://github.com/pandorafuture/wx-cli"
+4
View File
@@ -175,6 +175,10 @@ wx-cli server restart # 重启
REST 端点:`/api/v1/health``/api/v1/sessions``/api/v1/contacts``/api/v1/messages``/api/v1/timeline``/api/v1/search``/api/v1/media``/api/v1/events`SSE)。
`/api/v1/media` 的图片响应会通过 `X-Wechat-Media-Quality: full|thumbnail` 标明本机返回的是完整图还是缩略图;服务始终优先选择本机已经下载的高清/原图。
会话与联系人 JSON 在本地数据库有记录时会返回可选的 `avatar_url`,可用于展示个人或群聊头像;没有头像字段的旧数据库会省略该值。
其中 `/api/v1/timeline?since=<unix>&until=<unix>` 可在一次请求中读取时间范围内所有会话的消息,适合 Agent 记忆补全、归档和批处理,避免逐会话反复调用。
所有查询命令加 `--format json` 可获取 JSON 格式输出。
+5 -2
View File
@@ -263,6 +263,8 @@ wx-cli server status / stop / restart # 管理服务
| `GET /api/v1/search` | 全文搜索 | `q`(必填), `limit`, `offset` |
| `GET /api/v1/events` | SSE 事件流 | 无 |
图片响应会携带 `X-Wechat-Media-Quality: full|thumbnail`。服务会优先返回本机已有的高清/原图文件;若微信只下载过缩略图,则返回 `thumbnail`,调用方不应长期缓存,并可在微信下载原图后重试。
**认证:**`--token` 启动时须携带 `Authorization: Bearer <token>``--host` 不是本机地址时 `--token` 必填。
当前 HTTP API 为**只读**,没有 send/reply/webhook 等写接口。
@@ -312,14 +314,15 @@ wx-cli sessions # 4. 重试查询
| 命令 | item 关键字段 |
|------|-------------|
| sessions | `username`, `display_name`, `summary`, `sort_timestamp`, `direction?` |
| sessions | `username`, `display_name`, `avatar_url?`, `summary`, `sort_timestamp`, `direction?` |
| query | `sort_seq`, `server_id`, `msg_type`, `sender`, `content`, `direction` |
| timeline API | `sort_seq`, `server_id`, `msg_type`, `sender`, `talker`, `talker_display_name`, `create_time`, `direction`, `snippet`;统一按时间跨会话排序 |
| contacts | `user_name`, `alias`, `remark`, `nick_name`, `phone`, `labels` |
| contacts | `user_name`, `alias`, `remark`, `nick_name`, `avatar_url?`, `phone`, `labels` |
| search | `server_id`, `talker`, `sender`, `snippet`, `hit_type` |
**易混淆字段:**
- contacts 用 `user_name`(下划线),sessions 用 `username`(无下划线)
- `avatar_url` 优先取微信小头像地址,缺失时回退大头像地址;本地没有记录时不输出
- message 的 `sender` 才是消息级 self/other 判断依据
- `/api/v1/health``current_account.wxid` 是判断"我发的"的主事实源
+2
View File
@@ -47,6 +47,7 @@ mod tests {
last_sender_display_name: None,
},
display_name: "wxid_friend".to_string(),
avatar_url: None,
direction: None,
detected_at: None,
},
@@ -70,6 +71,7 @@ mod tests {
last_sender_display_name: None,
},
display_name: "wxid_friend".to_string(),
avatar_url: None,
direction: None,
detected_at: Some(2),
},
+19
View File
@@ -42,6 +42,7 @@ enum MediaPayload {
InlineBytes {
bytes: Vec<u8>,
content_type: &'static str,
quality: Option<&'static str>,
},
ServePath {
path: PathBuf,
@@ -56,11 +57,17 @@ impl MediaPayload {
Self::InlineBytes {
bytes,
content_type,
quality,
} => {
let mut response = bytes.into_response();
response
.headers_mut()
.insert(CONTENT_TYPE, HeaderValue::from_static(content_type));
if let Some(quality) = quality {
response
.headers_mut()
.insert("x-wechat-media-quality", HeaderValue::from_static(quality));
}
Ok(response)
}
Self::ServePath {
@@ -242,6 +249,14 @@ fn resolve_image(
let dat_path = lookup.recommended.ok_or_else(|| {
ServeError::NotFound(format!("no candidate image file found for md5={md5}"))
})?;
let quality = if dat_path
.file_name()
.is_some_and(|name| name.to_string_lossy().ends_with("_t.dat"))
{
"thumbnail"
} else {
"full"
};
let data = std::fs::read(&dat_path)
.map_err(|e| ServeError::Internal(format!("failed to read {}: {e}", dat_path.display())))?;
let decoded = wx_media::decrypt_dat(&data, &dat_decrypt)
@@ -259,12 +274,14 @@ fn resolve_image(
return Ok(MediaPayload::InlineBytes {
bytes: transcoded.data,
content_type: image_content_type(transcoded.ext),
quality: Some(quality),
});
}
Ok(MediaPayload::InlineBytes {
bytes: decoded.data,
content_type: image_content_type(&decoded.ext),
quality: Some(quality),
})
}
@@ -288,6 +305,7 @@ fn resolve_voice(
return Ok(MediaPayload::InlineBytes {
bytes: cached.bytes.clone(),
content_type: cached.content_type,
quality: None,
});
}
}
@@ -331,6 +349,7 @@ fn resolve_voice(
return Ok(MediaPayload::InlineBytes {
bytes: result.data,
content_type: result.mime,
quality: None,
});
}
Err(wx_media::MediaError::LookupMiss(_))
+1
View File
@@ -65,6 +65,7 @@ fn format_watch_line(
}
#[cfg(test)]
#[allow(clippy::items_after_test_module)]
mod tests {
use super::*;
+9
View File
@@ -17,6 +17,8 @@ pub struct EnrichedSession {
pub session: Session,
pub display_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub direction: Option<Direction>,
#[serde(skip_serializing_if = "Option::is_none")]
pub detected_at: Option<i64>,
@@ -61,10 +63,12 @@ pub fn enrich_session(
detected_at: Option<i64>,
) -> EnrichedSession {
let display_name = resolver.display_with_id(&session.username);
let avatar_url = resolver.avatar_url(&session.username).map(str::to_string);
let direction = derive_session_direction(session.last_msg_sender.as_deref(), self_wxid);
EnrichedSession {
session,
display_name,
avatar_url,
direction,
detected_at,
}
@@ -76,6 +80,7 @@ pub fn enrich_session_event(
resolver: &ContactResolver,
) -> EnrichedSession {
let display_name = resolver.display_with_id(&ev.username);
let avatar_url = resolver.avatar_url(&ev.username).map(str::to_string);
let direction = derive_session_direction(ev.last_msg_sender.as_deref(), self_wxid);
let session = Session {
username: ev.username,
@@ -88,6 +93,7 @@ pub fn enrich_session_event(
EnrichedSession {
session,
display_name,
avatar_url,
direction,
detected_at: Some(ev.detected_at),
}
@@ -743,6 +749,7 @@ mod tests {
last_sender_display_name: None,
},
display_name: "Spam".to_string(),
avatar_url: None,
direction: Some(Direction::Incoming),
detected_at: None,
};
@@ -764,6 +771,7 @@ mod tests {
last_sender_display_name: Some("Spammer".to_string()),
},
display_name: "Group".to_string(),
avatar_url: None,
direction: Some(Direction::Incoming),
detected_at: None,
};
@@ -787,6 +795,7 @@ mod tests {
last_sender_display_name: Some("Normal".to_string()),
},
display_name: "Group".to_string(),
avatar_url: None,
direction: Some(Direction::Incoming),
detected_at: None,
};
+42
View File
@@ -4,6 +4,7 @@ use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::path::Path;
use std::process::{Child, Command, Stdio};
use std::sync::Mutex;
use std::thread;
use std::time::{Duration, Instant};
@@ -18,6 +19,7 @@ const MSG_TABLE: &str = "Msg_29a6db07e8bbdb53f5d54cc3c309f3f1";
const GROUP_TALKER: &str = "test@chatroom";
const GROUP_MSG_TABLE: &str = "Msg_1d282e28b02b5c9f9522f855de32f9a8";
const HIDDEN_SENDER: &str = "wxid_spam";
static SERVER_START_LOCK: Mutex<()> = Mutex::new(());
fn bin() -> &'static str {
env!("CARGO_BIN_EXE_wx-cli")
@@ -150,9 +152,22 @@ fn serve_media_dispatch_image_returns_png_bytes() {
);
assert_eq!(response.status_code, 200, "{response:#?}");
assert_eq!(response.header("content-type"), Some("image/png"));
assert_eq!(response.header("x-wechat-media-quality"), Some("full"));
assert_eq!(&response.body[..8], b"\x89PNG\r\n\x1a\n");
}
#[test]
fn serve_media_thumbnail_only_image_reports_quality() {
let server = spawn_test_server();
let response = http_get(
&server.base_url,
"/api/v1/media?server_id=3009&talker=wxid_alice",
);
assert_eq!(response.status_code, 200, "{response:#?}");
assert_eq!(response.header("content-type"), Some("image/png"));
assert_eq!(response.header("x-wechat-media-quality"), Some("thumbnail"));
}
#[test]
fn serve_media_wxgf_embedded_png_returns_png_without_ffmpeg() {
let server = spawn_test_server_with_env(&[("FFMPEG_PATH", "/definitely-missing-ffmpeg")]);
@@ -326,6 +341,10 @@ fn spawn_test_server_with_setup(envs: &[(&str, &str)], hidden_contacts: &[&str])
}
let account_dir = fixture.path().join(TEST_ACCOUNT_ID);
let runtime_root = fixture.path().join("runtime");
// Keep ephemeral-port selection and worker binding atomic across parallel tests.
let _start_guard = SERVER_START_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let port = find_open_port();
let mut command = Command::new(bin());
command
@@ -712,6 +731,27 @@ fn create_encrypted_message_db(path: &Path, raw_key: &[u8; 32]) {
)
.expect("insert image ok message");
let image_thumb_only_info =
encode_packed_info_for_test(Some("md5_image_thumb_only"), None);
conn.execute(
&format!(
"INSERT INTO [{table}] VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
table = MSG_TABLE
),
params![
301_i64,
3009_i64,
3_i64,
1_i64,
1_709_251_200_i64,
Vec::<u8>::new(),
image_thumb_only_info,
0_i32,
None::<i32>,
],
)
.expect("insert thumbnail-only image message");
let image_wxgf_png_info = encode_packed_info_for_test(Some("md5_image_wxgf_png"), None);
conn.execute(
&format!(
@@ -985,6 +1025,8 @@ fn create_image_fixture(attach_dir: &Path) {
let encrypted = xor_bytes(&png, xor_key);
fs::write(month_dir.join("md5_image_ok_t.dat"), &encrypted).expect("write thumb dat");
fs::write(month_dir.join("md5_image_ok.dat"), &encrypted).expect("write image dat");
fs::write(month_dir.join("md5_image_thumb_only_t.dat"), &encrypted)
.expect("write thumbnail-only dat");
let wxgf_png = sample_wxgf_with_embedded_png();
let wxgf_png_encrypted = xor_bytes(&wxgf_png, xor_key);
+11
View File
@@ -15,6 +15,7 @@ struct ResolvedContact {
signature: Option<String>,
region: Option<String>,
labels: Vec<String>,
avatar_url: Option<String>,
}
pub struct ContactResolver {
@@ -56,6 +57,7 @@ impl ContactResolver {
signature: c.signature,
region: c.region,
labels: c.labels,
avatar_url: c.avatar_url,
},
);
}
@@ -92,6 +94,13 @@ impl ContactResolver {
.unwrap_or(&[])
}
/// Resolve a wxid to its preferred avatar URL.
pub fn avatar_url(&self, wxid: &str) -> Option<&str> {
self.contacts
.get(wxid)
.and_then(|contact| contact.avatar_url.as_deref())
}
/// Iterate all contacts with their wxid and labels.
/// Used by VisibilityIndex to expand ignore_tags.
pub fn all_labels(&self) -> impl Iterator<Item = (&String, &[String])> {
@@ -200,6 +209,7 @@ mod tests {
signature: None,
region: None,
labels: Vec::new(),
avatar_url: None,
},
)
})
@@ -235,6 +245,7 @@ mod tests {
signature: None,
region: None,
labels: labels.iter().map(|s| s.to_string()).collect(),
avatar_url: None,
},
)
})
+34 -8
View File
@@ -43,16 +43,19 @@ impl WechatDb {
limit: usize,
label_map: &HashMap<String, String>,
) -> Result<QueryResult<Contact>, DbError> {
let avatar_expr = self.contact_avatar_select_expr()?;
let total_rows: usize =
self.contact_conn
.query_row("SELECT COUNT(*) FROM contact", [], |row| {
row.get::<_, i64>(0)
})? as usize;
let mut stmt = self.contact_conn.prepare(
"SELECT username, alias, remark, nick_name, description, extra_buffer \
FROM contact ORDER BY username ASC LIMIT ?1 OFFSET ?2",
)?;
let sql = format!(
"SELECT username, alias, remark, nick_name, description, extra_buffer, \
{avatar_expr} AS avatar_url \
FROM contact ORDER BY username ASC LIMIT ?1 OFFSET ?2"
);
let mut stmt = self.contact_conn.prepare(&sql)?;
let rows = stmt.query_map(
[
Value::Integer(limit as i64),
@@ -81,6 +84,7 @@ impl WechatDb {
label_map: &HashMap<String, String>,
) -> Result<QueryResult<Contact>, DbError> {
let kw_lower = query.keyword.as_ref().unwrap().to_lowercase();
let avatar_expr = self.contact_avatar_select_expr()?;
let total_rows: usize =
self.contact_conn
@@ -88,10 +92,12 @@ impl WechatDb {
row.get::<_, i64>(0)
})? as usize;
let mut stmt = self.contact_conn.prepare(
"SELECT username, alias, remark, nick_name, description, extra_buffer \
FROM contact ORDER BY username ASC",
)?;
let sql = format!(
"SELECT username, alias, remark, nick_name, description, extra_buffer, \
{avatar_expr} AS avatar_url \
FROM contact ORDER BY username ASC"
);
let mut stmt = self.contact_conn.prepare(&sql)?;
let rows = stmt.query_map([], |row| self.map_contact_row(row, label_map))?;
let all_contacts: Vec<Contact> = rows.filter_map(|r| r.ok()).collect();
@@ -126,6 +132,10 @@ impl WechatDb {
let nick_name: String = row.get::<_, String>(3).unwrap_or_default();
let memo: Option<String> = row.get::<_, Option<String>>(4).unwrap_or(None);
let extra_buffer: Vec<u8> = row.get::<_, Vec<u8>>(5).unwrap_or_default();
let avatar_url: Option<String> = row
.get::<_, Option<String>>(6)
.unwrap_or(None)
.filter(|value| !value.is_empty());
let extra = contact_proto::decode_extra_buffer(&extra_buffer);
@@ -147,6 +157,22 @@ impl WechatDb {
source_scene: extra.source_scene,
phone: extra.phone,
labels,
avatar_url,
})
}
/// Build a compatible avatar expression for WeChat database variants.
/// Older fixtures and database versions may not contain either column.
fn contact_avatar_select_expr(&self) -> Result<&'static str, DbError> {
let has_small =
decode::check_column_exists(&self.contact_conn, "contact", "small_head_url")?;
let has_big = decode::check_column_exists(&self.contact_conn, "contact", "big_head_url")?;
Ok(match (has_small, has_big) {
(true, true) => "COALESCE(NULLIF(small_head_url, ''), NULLIF(big_head_url, ''))",
(true, false) => "NULLIF(small_head_url, '')",
(false, true) => "NULLIF(big_head_url, '')",
(false, false) => "NULL",
})
}
+3
View File
@@ -380,6 +380,9 @@ pub struct Contact {
pub phone: Option<String>,
/// Resolved label names from extra_buffer + contact_label table.
pub labels: Vec<String>,
/// Preferred avatar URL from `small_head_url`, falling back to `big_head_url`.
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar_url: Option<String>,
}
/// A WeChat chatroom (group chat) entry with its member list.
+49
View File
@@ -282,6 +282,55 @@ fn contacts_sessions_query_contacts_limit() {
assert_eq!(result.items.len(), 1);
}
#[test]
fn contacts_sessions_query_contacts_avatar_url() {
let dir = create_fixture();
let contact_path = dir.path().join("contact").join("contact.db");
let conn = Connection::open(contact_path).unwrap();
conn.execute_batch(
"ALTER TABLE contact ADD COLUMN small_head_url TEXT;
ALTER TABLE contact ADD COLUMN big_head_url TEXT;",
)
.unwrap();
conn.execute(
"UPDATE contact SET small_head_url = ?1, big_head_url = ?2 WHERE username = ?3",
params![
"https://avatar.example/alice-small.jpg",
"https://avatar.example/alice-big.jpg",
"wxid_alice"
],
)
.unwrap();
conn.execute(
"UPDATE contact SET big_head_url = ?1 WHERE username = ?2",
params!["https://avatar.example/bob-big.jpg", "wxid_bob"],
)
.unwrap();
drop(conn);
let db = WechatDb::open(dir.path()).unwrap();
let result = db.query_contacts(&ContactQuery::new()).unwrap();
let alice = result
.items
.iter()
.find(|contact| contact.user_name == "wxid_alice")
.unwrap();
let bob = result
.items
.iter()
.find(|contact| contact.user_name == "wxid_bob")
.unwrap();
assert_eq!(
alice.avatar_url.as_deref(),
Some("https://avatar.example/alice-small.jpg")
);
assert_eq!(
bob.avatar_url.as_deref(),
Some("https://avatar.example/bob-big.jpg")
);
}
// ---- sessions tests ----
#[test]
+12 -12
View File
@@ -15,21 +15,21 @@ fn make_xor_dat(key: u8) -> Vec<u8> {
/// Header: 07 08 V1 08 07 (6B) + aes_size LE (4B) + xor_size LE (4B) + 0x01 (1B) = 15B
/// Then AES-ECB encrypted payload with fixed key, then raw, then XOR tail.
fn make_v1_dat() -> Vec<u8> {
use aes::cipher::{BlockCipherEncrypt, KeyInit};
use aes::cipher::{Array, BlockCipherEncrypt, KeyInit};
use aes::Aes128;
let key = b"cfcd208495d565ef"; // md5("0")[:16]
let cipher = Aes128::new(key.into());
// Plaintext: JPEG header (16 bytes = 1 AES block) with PKCS7 padding
let mut block1 = [
let mut block1 = Array::from([
0xFFu8, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00,
0x01,
];
let mut block2 = [16u8; 16]; // full PKCS7 padding block
]);
let mut block2 = Array::from([16u8; 16]); // full PKCS7 padding block
cipher.encrypt_block(aes::cipher::Array::from_mut_slice(&mut block1));
cipher.encrypt_block(aes::cipher::Array::from_mut_slice(&mut block2));
cipher.encrypt_block(&mut block1);
cipher.encrypt_block(&mut block2);
let aes_size: u32 = 16; // original plaintext size
let xor_size: u32 = 0;
@@ -46,20 +46,20 @@ fn make_v1_dat() -> Vec<u8> {
/// Build a V2-encrypted `.dat` file with known AES key and XOR tail.
fn make_v2_dat(aes_key: &[u8; 16], xor_key: u8) -> Vec<u8> {
use aes::cipher::{BlockCipherEncrypt, KeyInit};
use aes::cipher::{Array, BlockCipherEncrypt, KeyInit};
use aes::Aes128;
let cipher = Aes128::new(aes_key.into());
// Plaintext: PNG header (16 bytes = 1 block)
let mut block1 = [
let mut block1 = Array::from([
0x89u8, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44,
0x52,
];
let mut block2 = [16u8; 16]; // PKCS7 padding block
]);
let mut block2 = Array::from([16u8; 16]); // PKCS7 padding block
cipher.encrypt_block(aes::cipher::Array::from_mut_slice(&mut block1));
cipher.encrypt_block(aes::cipher::Array::from_mut_slice(&mut block2));
cipher.encrypt_block(&mut block1);
cipher.encrypt_block(&mut block2);
let aes_size: u32 = 16;
// Tail: 4 bytes XOR-encrypted
+3 -3
View File
@@ -369,21 +369,21 @@ mod tests {
#[test]
fn server_lock_file_under_server_state() {
let ap = AppPaths::new().unwrap();
assert!(ap.server_lock_file().starts_with(&ap.server_state_dir()));
assert!(ap.server_lock_file().starts_with(ap.server_state_dir()));
assert!(ap.server_lock_file().ends_with("manager.lock"));
}
#[test]
fn server_config_file_under_server_state() {
let ap = AppPaths::new().unwrap();
assert!(ap.server_config_file().starts_with(&ap.server_state_dir()));
assert!(ap.server_config_file().starts_with(ap.server_state_dir()));
assert!(ap.server_config_file().ends_with("config.json"));
}
#[test]
fn server_state_file_under_server_state() {
let ap = AppPaths::new().unwrap();
assert!(ap.server_state_file().starts_with(&ap.server_state_dir()));
assert!(ap.server_state_file().starts_with(ap.server_state_dir()));
assert!(ap.server_state_file().ends_with("state.json"));
}