diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..8eaea5c --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,48 @@ +name: build +on: + workflow_dispatch: + +jobs: + build: + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Setup toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: i686-pc-windows-msvc + components: rustfmt, clippy + + - name: Build + uses: actions-rs/cargo@v1 + with: + command: build + args: --release --target=i686-pc-windows-msvc + + - name: Prepare for upload + run: | + mkdir artifact + mkdir artifact/saori-qrcode + copy target/i686-pc-windows-msvc/release/saori_qrcode.dll artifact/saori-qrcode/saori_qrcode.dll + copy README.md artifact/saori-qrcode/README.md + copy LICENSE artifact/saori-qrcode/LICENSE + + - name: Upload artifact + uses: actions/upload-artifact@v3 + with: + name: saori-qrcode + path: artifact/ diff --git a/.github/workflows/check_and_test.yml b/.github/workflows/check_and_test.yml new file mode 100644 index 0000000..4189e61 --- /dev/null +++ b/.github/workflows/check_and_test.yml @@ -0,0 +1,40 @@ +name: check-and-test +on: + push: + branches: + - main + +jobs: + check-and-test: + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup cache + uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Setup toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: i686-pc-windows-msvc + components: rustfmt, clippy + + - name: Check + uses: actions-rs/cargo@v1 + with: + command: check + + - name: Test + uses: actions-rs/cargo@v1 + with: + command: test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7ea40f3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "saori-qrcode" +version = "0.1.0" +authors = ["Don"] +license-file = "LICENSE" +readme = "README.md" +edition = "2021" +description = "SAORI to make QR Code" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +qrcode = "0.14.0" +image = "0.25.0" + +[target.'cfg(windows)'.dependencies] +winapi = {version = "0.3.9", features = ["winbase", "libloaderapi", "stringapiset"]} + +[dev-dependencies] +tempfile = "3.3.0" +encoding_rs = "0.8.31" + +[lib] +name = "saori_qrcode" +path = "src/lib.rs" +crate-type = ["rlib", "cdylib"] + +[profile.release] +strip = true +opt-level = "z" +lto = true +codegen-units = 1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3452baf --- /dev/null +++ b/LICENSE @@ -0,0 +1,194 @@ +# saori-qrcode CC0-1.0 License + +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. + +# saori-resized-png MIT License + +Copyright (c) 2022 月波 清火 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +# qrcode-rust MIT OR Apache-2.0 License + +Copyright (c) 2016 kennytm + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +# image-rs/image MIT OR Apache-2.0 License + +MIT License + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c5d06f5 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# saori_qrcode.dll + +[GitHub repository](https://github.com/nikolat/saori-qrcode) + +## これは何? + +デスクトップマスコット、「伺か」で使用できるSAORIの一種です。 + +入力したテキストをQR Codeとして出力します。 + +## 使い方 + +Argument0に、使用する機能名を指定して使用します。 +指定できる機能は`image`と`text`です。 + +### `image` + ++ Argument1: QR Codeに変換したいテキスト ++ Argument2: 画像として出力するファイルのパス + ++ Result: 無し + +入力されたテキストのQR Codeを画像ファイル(PNG)として指定されたファイルのパスに出力します。 + +### `text` + ++ Argument1: QR Codeに変換したいテキスト + ++ Result: テキスト形式で整形されたQR Code + +入力されたテキストのQR Codeを`\n`を改行コードと見立てて` `および`#`で表現したテキストを返します。 + +## 使用ライブラリ/参考にしたプロジェクト + ++ [saori-resized-png](https://github.com/tukinami/saori-resized-png) / 月波 清火 (tukinami seika) ++ [qrcode-rust](https://github.com/kennytm/qrcode-rust) / kennytm ++ [image](https://github.com/image-rs/image) / The image-rs Developers ++ (テスト実行時) [encoding\_rs](https://github.com/hsivonen/encoding_rs) / Henri Sivonen ++ (テスト実行時) [tempfile](https://github.com/Stebalien/tempfile) / Steven Allen, The Rust Project Developers, Ashley Mannix, Jason White + +## ライセンス + +LICENSEファイルを見てください。 + +## 作成者 + +Don + +[GitHub](https://github.com/nikolat) diff --git a/src/chars.rs b/src/chars.rs new file mode 100644 index 0000000..042585b --- /dev/null +++ b/src/chars.rs @@ -0,0 +1,139 @@ +use winapi::{ + shared::{ + minwindef::LPBOOL, + ntdef::{LPCSTR, LPWSTR, NULL}, + }, + um::{ + stringapiset::{MultiByteToWideChar, WideCharToMultiByte}, + winnls::MB_PRECOMPOSED, + winnt::LPSTR, + }, +}; + +pub(crate) fn multi_byte_to_wide_char(from: &[u8], codepage: u32) -> Result, ()> { + let mut from_buf: Vec = from.iter().map(|v| *v as i8).collect(); + from_buf.push(0); + + let to_buf_size = unsafe { + MultiByteToWideChar( + codepage, + MB_PRECOMPOSED, + from_buf.as_ptr(), + -1, + NULL as LPWSTR, + 0, + ) + }; + + if to_buf_size == 0 { + return Err(()); + } + + let mut to_buf = vec![0; to_buf_size as usize + 1]; + let result = unsafe { + MultiByteToWideChar( + codepage, + MB_PRECOMPOSED, + from_buf.as_ptr(), + -1, + to_buf.as_mut_ptr(), + to_buf_size, + ) + }; + + if result == 0 { + Err(()) + } else { + Ok(to_buf) + } +} + +pub(crate) fn wide_char_to_multi_byte(from: &mut Vec, codepage: u32) -> Result, ()> { + from.push(0); + + let to_buf_size = unsafe { + WideCharToMultiByte( + codepage, + 0, + from.as_ptr(), + -1, + NULL as LPSTR, + 0, + NULL as LPCSTR, + NULL as LPBOOL, + ) + }; + + if to_buf_size == 0 { + return Err(()); + } + + let mut to_buf: Vec = vec![0; to_buf_size as usize + 1]; + let result = unsafe { + WideCharToMultiByte( + codepage, + 0, + from.as_ptr(), + -1, + to_buf.as_mut_ptr(), + to_buf_size, + NULL as LPCSTR, + NULL as LPBOOL, + ) + }; + + if result == 0 { + Err(()) + } else { + Ok(to_buf) + } +} + +#[cfg(test)] +mod tests { + use crate::request::SaoriCharset; + + use super::*; + + use encoding_rs; + + mod multi_byte_to_wide_char { + use super::*; + + #[test] + fn success_when_encoding_and_codepage_is_same() { + let case = "あいうえお仕様"; + let (case_byte, _encoding, _is_err) = encoding_rs::SHIFT_JIS.encode(case); + + let result = + multi_byte_to_wide_char(&case_byte, SaoriCharset::ShiftJIS.codepage()).unwrap(); + + let p = result.partition_point(|v| *v != 0); + + let result = String::from_utf16_lossy(&result[..p]); + + assert_eq!(&result, case); + } + } + + mod wide_char_to_multi_byte { + use super::*; + + #[test] + fn success_when_valid_wide_char_and_codepage_with_shift_jis() { + let case = "あいうえお仕様"; + let mut case_chars: Vec = case.encode_utf16().collect(); + + let result = + wide_char_to_multi_byte(&mut case_chars, SaoriCharset::ShiftJIS.codepage()) + .unwrap(); + + let result: Vec = result.iter().map(|v| *v as u8).collect(); + + let p = result.partition_point(|v| *v != 0); + let (encoded, _encoding, _is_err) = encoding_rs::SHIFT_JIS.decode(&result[..p]); + + assert_eq!(&encoded, case); + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d20a676 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,129 @@ +mod chars; +mod procedure; +mod request; +mod qrcode; +mod response; + +use winapi::ctypes::c_long; +use winapi::shared::minwindef::{BOOL, DWORD, HGLOBAL, HINSTANCE, LPVOID, MAX_PATH, TRUE}; +use winapi::um::libloaderapi::GetModuleFileNameW; +use winapi::um::winbase::{GlobalAlloc, GlobalFree, GMEM_FIXED}; +use winapi::um::winnt::{ + DLL_PROCESS_ATTACH, DLL_PROCESS_DETACH, DLL_THREAD_ATTACH, DLL_THREAD_DETACH, +}; + +use std::slice; + +use crate::request::{SaoriCommand, SaoriRequest}; +use crate::response::SaoriResponse; + +static mut DLL_PATH: String = String::new(); + +#[no_mangle] +pub extern "system" fn DllMain( + h_module: HINSTANCE, + ul_reason_for_call: DWORD, + _l_reserved: LPVOID, +) -> BOOL { + match ul_reason_for_call { + DLL_PROCESS_ATTACH => { + register_dll_path(h_module); + } + DLL_PROCESS_DETACH => {} + DLL_THREAD_ATTACH => {} + DLL_THREAD_DETACH => { + unload(); + } + _ => {} + } + return TRUE; +} + +fn register_dll_path(h_module: HINSTANCE) { + let mut buf: [u16; MAX_PATH + 1] = [0; MAX_PATH + 1]; + unsafe { + GetModuleFileNameW(h_module, buf.as_mut_ptr(), MAX_PATH as u32); + } + + let p = buf.partition_point(|v| *v != 0); + + unsafe { + DLL_PATH = String::from_utf16_lossy(&buf[..p]); + } +} + +#[no_mangle] +pub extern "cdecl" fn load(h: HGLOBAL, _len: c_long) -> BOOL { + unsafe { GlobalFree(h) }; + + unsafe { procedure::load(&DLL_PATH) }; + + return TRUE; +} + +#[no_mangle] +pub extern "cdecl" fn unload() -> BOOL { + unsafe { procedure::unload(&DLL_PATH) }; + return TRUE; +} + +#[no_mangle] +pub extern "cdecl" fn request(h: HGLOBAL, len: *mut c_long) -> HGLOBAL { + // リクエストの取得 + let s = unsafe { hglobal_to_vec_u8(h, *len) }; + unsafe { GlobalFree(h) }; + + let request = SaoriRequest::from_u8(&s); + + // 返答の組み立て + let mut response = match &request { + Ok(r) => SaoriResponse::from_request(r), + Err(_e) => SaoriResponse::new_bad_request(), + }; + + if let Ok(r) = request { + match r.command() { + SaoriCommand::GetVersion => { + unsafe { procedure::get_version(&DLL_PATH, &r, &mut response) }; + } + SaoriCommand::Execute => { + unsafe { procedure::execute(&DLL_PATH, &r, &mut response) }; + } + } + } + + let response_bytes = response.to_encoded_bytes().unwrap_or(Vec::new()); + + let response = slice_i8_to_hglobal(len, &response_bytes); + + return response; +} + +fn slice_i8_to_hglobal(h_len: *mut c_long, data: &[i8]) -> HGLOBAL { + let data_len = data.len(); + + let h = unsafe { GlobalAlloc(GMEM_FIXED, data_len) }; + + unsafe { *h_len = data_len as c_long }; + + let h_slice = unsafe { slice::from_raw_parts_mut(h as *mut i8, data_len) }; + + for (index, value) in data.iter().enumerate() { + h_slice[index] = *value; + } + + return h; +} + +fn hglobal_to_vec_u8(h: HGLOBAL, len: c_long) -> Vec { + let mut s = vec![0; len as usize + 1]; + + let slice = unsafe { slice::from_raw_parts(h as *const u8, len as usize) }; + + for (index, value) in slice.iter().enumerate() { + s[index] = *value; + } + s[len as usize] = b'\0'; + + return s; +} diff --git a/src/procedure.rs b/src/procedure.rs new file mode 100644 index 0000000..e19b014 --- /dev/null +++ b/src/procedure.rs @@ -0,0 +1,45 @@ +use std::path::PathBuf; + +use crate::qrcode::{get_image, get_text}; +use crate::request::*; +use crate::response::*; + +/// load時に呼ばれる関数 +pub fn load(_path: &str) {} + +/// unload時に呼ばれる関数 +pub fn unload(_path: &str) {} + +/// request GET Version時に呼ばれる関数 +pub fn get_version(_path: &str, _request: &SaoriRequest, response: &mut SaoriResponse) { + response.set_result(String::from(env!("CARGO_PKG_VERSION"))); +} + +/// request EXECUTE時に呼ばれる関数 +/// メインの処理はここに記述する +pub fn execute(path: &str, request: &SaoriRequest, response: &mut SaoriResponse) { + let args = request.argument(); + let mut path = PathBuf::from(path); + if !path.is_dir() { + path.pop(); + } + + if let Some(func) = args.get(0) { + match func.as_str() { + "image" => { + if let (Some(text), Some(output_path_str)) = (args.get(1), args.get(2)) { + let output_path = path.join(output_path_str); + get_image(text, &output_path); + response.set_result("".to_string()); + } + } + "text" => { + if let Some(text) = args.get(1) { + let v = get_text(text); + response.set_result(v); + } + } + _ => {} + } + } +} diff --git a/src/qrcode.rs b/src/qrcode.rs new file mode 100644 index 0000000..fb4166d --- /dev/null +++ b/src/qrcode.rs @@ -0,0 +1,21 @@ +use image::Luma; +use qrcode::QrCode; +use std::path::PathBuf; + +pub(crate) fn get_image(text: &str, dist_path: &PathBuf) { + let code = QrCode::new(text).unwrap(); + let image = code.render::>().build(); + image.save(dist_path).unwrap(); +} + +pub(crate) fn get_text(text: &str) -> String { + let code = QrCode::new(text).unwrap(); + let string = code + .render() + .light_color(' ') + .dark_color('#') + .build() + .escape_debug() + .to_string(); + return string; +} diff --git a/src/request.rs b/src/request.rs new file mode 100644 index 0000000..bc6e0a1 --- /dev/null +++ b/src/request.rs @@ -0,0 +1,505 @@ +use crate::chars::multi_byte_to_wide_char; + +#[derive(PartialEq, Debug, Clone)] +pub enum SaoriVersion { + V1_0, +} + +impl SaoriVersion { + pub fn to_str(&self) -> &'static str { + match self { + SaoriVersion::V1_0 => "SAORI/1.0", + } + } +} + +#[derive(PartialEq, Debug)] +pub enum SaoriCommand { + Execute, + GetVersion, +} + +impl SaoriCommand { + pub fn to_str(&self) -> &'static str { + match self { + SaoriCommand::Execute => "EXECUTE", + SaoriCommand::GetVersion => "GET Version", + } + } +} + +#[derive(PartialEq, Debug, Clone)] +pub enum SaoriSecurityLevel { + Local, + External, +} + +impl SaoriSecurityLevel { + pub fn to_str(&self) -> &'static str { + match self { + SaoriSecurityLevel::Local => "Local", + SaoriSecurityLevel::External => "External", + } + } +} + +#[derive(PartialEq, Debug, Clone)] +pub enum SaoriCharset { + ShiftJIS, + EucJP, + UTF8, + ISO2022JP, +} + +impl SaoriCharset { + pub fn to_str(&self) -> &'static str { + match self { + SaoriCharset::ShiftJIS => "Shift_JIS", + SaoriCharset::EucJP => "EUC-JP", + SaoriCharset::UTF8 => "UTF-8", + SaoriCharset::ISO2022JP => "ISO-2022-JP", + } + } + + pub fn codepage(&self) -> u32 { + match self { + SaoriCharset::ShiftJIS => 932, + SaoriCharset::EucJP => 20932, + SaoriCharset::UTF8 => 65001, + SaoriCharset::ISO2022JP => 50222, + } + } +} + +#[derive(Debug, PartialEq)] +pub enum SaoriRequestError { + Charset(SaoriRequestCharsetError), + VersionLine(SaoriRequestVersionLineError), + Argument(SaoriRequestArgumentError), +} + +impl From for SaoriRequestError { + fn from(e: SaoriRequestCharsetError) -> SaoriRequestError { + SaoriRequestError::Charset(e) + } +} + +impl From for SaoriRequestError { + fn from(e: SaoriRequestVersionLineError) -> SaoriRequestError { + SaoriRequestError::VersionLine(e) + } +} + +impl From for SaoriRequestError { + fn from(e: SaoriRequestArgumentError) -> SaoriRequestError { + SaoriRequestError::Argument(e) + } +} + +#[derive(Debug, PartialEq)] +pub enum SaoriRequestCharsetError { + DecodeFailed, +} + +#[derive(Debug, PartialEq)] +pub enum SaoriRequestVersionLineError { + EmptyRequest, + NoVersion, + NoCommand, +} + +#[derive(Debug, PartialEq)] +pub enum SaoriRequestArgumentError { + InvalidSeparator, + NoIndex, +} + +#[derive(PartialEq, Debug)] +pub struct SaoriRequest { + version: SaoriVersion, + command: SaoriCommand, + security_level: Option, + argument: Vec, + charset: SaoriCharset, + sender: Option, +} + +impl SaoriRequest { + pub fn from_u8(from: &[u8]) -> Result { + let (body, charset) = SaoriRequest::decode_u8(from)?; + + let (version, command) = SaoriRequest::parse_version_line(&body)?; + + let lines = body.lines(); + let mut security_level = None; + let mut argument = Vec::new(); + let mut sender = None; + + for line in lines { + security_level = security_level.or(SaoriRequest::parse_security_level(line)); + SaoriRequest::parse_argument(line, &mut argument)?; + sender = sender.or(SaoriRequest::parse_sender(line)); + } + + Ok(SaoriRequest { + version, + command, + security_level, + argument, + charset, + sender, + }) + } + + pub fn version(&self) -> &SaoriVersion { + &self.version + } + + pub fn command(&self) -> &SaoriCommand { + &self.command + } + + #[allow(dead_code)] + pub fn security_level(&self) -> Option<&SaoriSecurityLevel> { + self.security_level.as_ref() + } + + #[allow(dead_code)] + pub fn argument(&self) -> &[String] { + &self.argument + } + + pub fn charset(&self) -> &SaoriCharset { + &self.charset + } + + #[allow(dead_code)] + pub fn sender(&self) -> Option<&String> { + self.sender.as_ref() + } + + /// リクエスト中のCharsetを処理し、デコードする関数。 + fn decode_u8(from: &[u8]) -> Result<(String, SaoriCharset), SaoriRequestCharsetError> { + let temp = String::from_utf8_lossy(from); + let temp_lines = temp.lines(); + + let mut charset = SaoriCharset::ShiftJIS; + + for line in temp_lines { + if line.starts_with("Charset: ") { + if line.ends_with(SaoriCharset::ShiftJIS.to_str()) { + charset = SaoriCharset::ShiftJIS; + } else if line.ends_with(SaoriCharset::EucJP.to_str()) { + charset = SaoriCharset::EucJP; + } else if line.ends_with(SaoriCharset::UTF8.to_str()) { + charset = SaoriCharset::UTF8; + } else if line.ends_with(SaoriCharset::ISO2022JP.to_str()) { + charset = SaoriCharset::ISO2022JP; + } else { + charset = SaoriCharset::ShiftJIS; + } + } + } + + let wide_chars = multi_byte_to_wide_char(from, charset.codepage()) + .map_err(|_| SaoriRequestCharsetError::DecodeFailed)?; + + let p = wide_chars.partition_point(|v| *v != 0); + + Ok((String::from_utf16_lossy(&wide_chars[..p]), charset)) + } + + /// リクエスト中のバージョン・コマンドを処理する関数。 + fn parse_version_line( + body: &str, + ) -> Result<(SaoriVersion, SaoriCommand), SaoriRequestVersionLineError> { + let first_line = if let Some(v) = body.lines().next() { + v + } else { + return Err(SaoriRequestVersionLineError::EmptyRequest); + }; + + let version = if first_line.ends_with(SaoriVersion::V1_0.to_str()) { + SaoriVersion::V1_0 + } else { + return Err(SaoriRequestVersionLineError::NoVersion); + }; + + let command = if first_line.starts_with(SaoriCommand::Execute.to_str()) { + SaoriCommand::Execute + } else if first_line.starts_with(SaoriCommand::GetVersion.to_str()) { + SaoriCommand::GetVersion + } else { + return Err(SaoriRequestVersionLineError::NoCommand); + }; + + return Ok((version, command)); + } + + /// リクエスト中のSecurityLevelを処理する関数。 + fn parse_security_level(line: &str) -> Option { + if line.starts_with("SecurityLevel: ") { + if line.ends_with(SaoriSecurityLevel::Local.to_str()) { + return Some(SaoriSecurityLevel::Local); + } else if line.ends_with(SaoriSecurityLevel::External.to_str()) { + return Some(SaoriSecurityLevel::External); + } else { + return None; + } + } else { + return None; + } + } + + /// リクエスト中のArgument*を処理する関数。 + fn parse_argument( + line: &str, + argument: &mut Vec, + ) -> Result<(), SaoriRequestArgumentError> { + if line.starts_with("Argument") { + // 行分離 + let mut split = line.splitn(2, ": "); + let (header, body) = match (split.next(), split.next()) { + (Some(h), Some(b)) => (h, b), + (_, _) => { + return Err(SaoriRequestArgumentError::InvalidSeparator); + } + }; + // 引数番号取得 + let index: String = header.chars().skip(8).collect(); + let index = if let Ok(v) = index.parse::() { + v + } else { + return Err(SaoriRequestArgumentError::NoIndex); + }; + // indexが入るようになるまでrequest.argumentを伸張する。 + while argument.len() <= index { + argument.push(String::new()); + } + // 引数取得 + argument[index] = body.to_string(); + } + Ok(()) + } + + /// リクエスト中のSenderを処理する関数 + fn parse_sender(line: &str) -> Option { + if line.starts_with("Sender: ") { + let body = line.replace("Sender: ", ""); + return Some(body); + } else { + return None; + } + } +} +#[cfg(test)] +mod tests { + use super::*; + + mod saori_request { + use super::*; + + mod from_u8 { + use super::*; + + use encoding_rs; + + #[test] + fn success_when_valid_request() { + let case = "EXECUTE SAORI/1.0\r\n +SecurityLevel: Local\r\n +Charset: Shift_JIS\r\n +Argument0: 零\r\n +\r\n\0 +"; + let (case_bytes, _encoding, _is_err) = encoding_rs::SHIFT_JIS.encode(case); + let expect = SaoriRequest { + version: SaoriVersion::V1_0, + command: SaoriCommand::Execute, + security_level: Some(SaoriSecurityLevel::Local), + argument: vec![String::from("零")], + charset: SaoriCharset::ShiftJIS, + sender: None, + }; + assert_eq!(SaoriRequest::from_u8(&case_bytes), Ok(expect)); + } + + #[test] + fn failed_when_invalid_request() { + let case = "SAORI/1.0\r\n +Argument0: 123\r\n +\r\n\0 +"; + assert_eq!( + SaoriRequest::from_u8(case.as_bytes()), + Err(SaoriRequestError::VersionLine( + SaoriRequestVersionLineError::NoCommand + )) + ); + } + } + + mod decode_u8 { + use super::*; + + use encoding_rs; + + #[test] + fn success_when_valid_bytes() { + let case = "EXECUTE SAORI/1.0\r\nCharset: Shift_JIS\r\nArgument0: 一\r\n\r\n"; + let case_string = format!("{}\0", case); + + let (case_bytes, _encoding, _is_err) = encoding_rs::SHIFT_JIS.encode(&case_string); + assert_eq!( + SaoriRequest::decode_u8(&case_bytes), + Ok((case.to_string(), SaoriCharset::ShiftJIS)) + ); + } + } + + mod parse_version_line { + use super::*; + + #[test] + fn success_when_valid_command_version() { + let case = "GET Version SAORI/1.0\r\n\r\n\0"; + assert_eq!( + SaoriRequest::parse_version_line(case), + Ok((SaoriVersion::V1_0, SaoriCommand::GetVersion)) + ); + } + + #[test] + fn success_when_valid_command_execute() { + let case = "EXECUTE SAORI/1.0\r\nCharset: UTF-8\r\nArgument0: 零\r\n\r\n\0"; + assert_eq!( + SaoriRequest::parse_version_line(case), + Ok((SaoriVersion::V1_0, SaoriCommand::Execute)) + ); + } + + #[test] + fn failed_when_enmpty_request() { + let case = ""; + assert_eq!( + SaoriRequest::parse_version_line(case), + Err(SaoriRequestVersionLineError::EmptyRequest) + ); + } + + #[test] + fn failed_when_no_command() { + let case = "SAORI/1.0\r\n\r\n\0"; + assert_eq!( + SaoriRequest::parse_version_line(case), + Err(SaoriRequestVersionLineError::NoCommand) + ); + } + + #[test] + fn failed_when_no_version() { + let case = "GET Version \r\n\r\n\0"; + assert_eq!( + SaoriRequest::parse_version_line(case), + Err(SaoriRequestVersionLineError::NoVersion) + ); + } + } + + mod parse_security_level { + use super::*; + + #[test] + fn some_value_when_valid_security_line() { + let case = "SecurityLevel: Local"; + assert_eq!( + SaoriRequest::parse_security_level(case), + Some(SaoriSecurityLevel::Local) + ); + + let case = "SecurityLevel: External"; + assert_eq!( + SaoriRequest::parse_security_level(case), + Some(SaoriSecurityLevel::External), + ); + } + + #[test] + fn none_when_it_is_not_security_line() { + let case = "Argument0: test"; + assert!(SaoriRequest::parse_security_level(case).is_none()); + } + } + + mod parse_argument { + use super::*; + + #[test] + fn success_when_valid_argument_lines() { + let mut arguments = Vec::new(); + + let case = "Argument123: 一二三"; + assert_eq!(SaoriRequest::parse_argument(case, &mut arguments), Ok(())); + assert_eq!(arguments.get(123), Some(&String::from("一二三"))); + + let case = "Argument124: 一二四"; + assert_eq!(SaoriRequest::parse_argument(case, &mut arguments), Ok(())); + let case = "Argument1: 一"; + assert_eq!(SaoriRequest::parse_argument(case, &mut arguments), Ok(())); + + assert_eq!(arguments.get(124), Some(&String::from("一二四"))); + assert_eq!(arguments.get(1), Some(&String::from("一"))); + assert_eq!(arguments.get(123), Some(&String::from("一二三"))); + } + + #[test] + fn success_when_line_is_empty() { + let mut arguments = Vec::new(); + + let case = ""; + assert_eq!(SaoriRequest::parse_argument(case, &mut arguments), Ok(())); + } + + #[test] + fn failed_when_invalid_separator() { + let mut arguments = Vec::new(); + + let case = "Argument 123"; + assert_eq!( + SaoriRequest::parse_argument(case, &mut arguments), + Err(SaoriRequestArgumentError::InvalidSeparator) + ); + } + + #[test] + fn failed_when_invalid_index() { + let mut arguments = Vec::new(); + + let case = "Argument: 123"; + assert_eq!( + SaoriRequest::parse_argument(case, &mut arguments), + Err(SaoriRequestArgumentError::NoIndex) + ); + } + } + + mod parse_sender { + use super::*; + + #[test] + fn some_value_when_valid_sender_line() { + let case = "Sender: materia"; + assert_eq!( + SaoriRequest::parse_sender(case), + Some(String::from("materia")) + ); + } + + #[test] + fn none_when_it_is_not_sender_line() { + let case = "Argument0: 123"; + assert!(SaoriRequest::parse_sender(case).is_none()); + } + } + } +} diff --git a/src/response.rs b/src/response.rs new file mode 100644 index 0000000..ba314ce --- /dev/null +++ b/src/response.rs @@ -0,0 +1,157 @@ +use crate::{ + chars::wide_char_to_multi_byte, + request::{SaoriCharset, SaoriRequest, SaoriVersion}, +}; + +#[derive(PartialEq, Debug)] +pub enum SaoriStatus { + OK, + NoContent, + BadRequest, + #[allow(dead_code)] + InternalServerError, +} + +impl SaoriStatus { + pub fn to_code(&self) -> u16 { + match self { + SaoriStatus::OK => 200, + SaoriStatus::NoContent => 204, + SaoriStatus::BadRequest => 400, + SaoriStatus::InternalServerError => 500, + } + } + + pub fn to_str(&self) -> &'static str { + match self { + SaoriStatus::OK => "OK", + SaoriStatus::NoContent => "No Content", + SaoriStatus::BadRequest => "Bad Request", + SaoriStatus::InternalServerError => "Internal Server Error", + } + } +} + +#[derive(PartialEq, Debug)] +pub enum SaoriResponseError { + DecodeFailed, +} + +#[derive(PartialEq, Debug)] +pub struct SaoriResponse { + version: SaoriVersion, + status: SaoriStatus, + result: String, + value: Vec, + charset: SaoriCharset, +} + +impl SaoriResponse { + /// status がBad Request である自身を生成する + pub fn new_bad_request() -> SaoriResponse { + SaoriResponse { + version: SaoriVersion::V1_0, + status: SaoriStatus::BadRequest, + result: String::new(), + value: Vec::new(), + charset: SaoriCharset::UTF8, + } + } + + /// リクエストから自身を生成する + pub fn from_request(request: &SaoriRequest) -> SaoriResponse { + SaoriResponse { + version: request.version().clone(), + status: SaoriStatus::NoContent, + result: String::new(), + value: Vec::new(), + charset: request.charset().clone(), + } + } + + #[allow(dead_code)] + pub fn status(&self) -> &SaoriStatus { + &self.status + } + #[allow(dead_code)] + pub fn set_status(&mut self, status: SaoriStatus) { + self.status = status; + } + + #[allow(dead_code)] + pub fn result(&self) -> &str { + &self.result + } + #[allow(dead_code)] + pub fn set_result(&mut self, result: String) { + self.result = result; + + self.on_change_result_and_value(); + } + + #[allow(dead_code)] + pub fn value(&self) -> &[String] { + &self.value + } + #[allow(dead_code)] + pub fn set_value(&mut self, value: Vec) { + self.value = value; + + self.on_change_result_and_value(); + } + + /// resultとvalueが変更されたときに呼ばれる + /// statusの切替を行う(Ok <=> No Content) + fn on_change_result_and_value(&mut self) { + match self.status { + SaoriStatus::BadRequest | SaoriStatus::InternalServerError => return, + _ => { + if !self.result.is_empty() || !self.value.is_empty() { + self.status = SaoriStatus::OK; + } else { + self.status = SaoriStatus::NoContent + } + } + } + } + + /// 自身をエンコードされた文字バイト列にして返す + pub fn to_encoded_bytes(&mut self) -> Result, SaoriResponseError> { + let req = self.to_string(); + + let mut wide_chars: Vec = req.encode_utf16().collect(); + + let result = wide_char_to_multi_byte(&mut wide_chars, self.charset.codepage()) + .map_err(|_| SaoriResponseError::DecodeFailed)?; + + Ok(result) + } + + /// 自身を文字列にして返す + fn to_string(&self) -> String { + let mut result = String::new(); + + result.push_str(&format!( + "{} {} {}\r\nCharset: {}\r\n", + self.version.to_str(), + self.status.to_code(), + self.status.to_str(), + self.charset.to_str() + )); + match self.status { + SaoriStatus::OK => { + if !self.result.is_empty() { + result.push_str(&format!("Result: {}\r\n", self.result)); + } + + for (index, value) in self.value.iter().enumerate() { + result.push_str(&format!("Value{}: {}\r\n", index, value)); + } + } + _ => {} + } + result.push_str("\r\n\0"); + + return result; + } +}