diff --git a/.changepacks/config.json b/.changepacks/config.json index 4eeb6d96..82e9ca5a 100644 --- a/.changepacks/config.json +++ b/.changepacks/config.json @@ -1,5 +1,5 @@ { - "ignore": ["**", "!packages/python/pyproject.toml", "!packages/dotnet/BraillifyNet/BraillifyNet.csproj", "!packages/dotnet/Braillify/Braillify.csproj", "!packages/node/package.json", "!packages/c/Cargo.toml", "!packages/jvm/build.gradle.kts", "!libs/braillify/Cargo.toml"], + "ignore": ["**", "!packages/python/pyproject.toml", "!packages/dotnet/BraillifyNet/BraillifyNet.csproj", "!packages/dotnet/Braillify/Braillify.csproj", "!packages/node/package.json", "!packages/c/Cargo.toml", "!packages/jvm/build.gradle.kts", "!libs/braillify/Cargo.toml", "!packages/go/Cargo.toml"], "baseBranch": "main", "latestPackage": null, "publish": {} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a518801f..5a69b94f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -307,6 +307,7 @@ jobs: needs: - test - dotnet-test + - go-test - jvm-test steps: - uses: actions/checkout@v7 @@ -893,7 +894,7 @@ jobs: name: nuget-packages path: packages/dotnet/**/nupkg/*.nupkg retention-days: 1 - + # c c-build: name: C Build - ${{ matrix.rid }} @@ -1030,6 +1031,191 @@ jobs: with: upload_url: ${{ fromJson(needs.changepacks.outputs.release_assets_urls)['packages/c/Cargo.toml'] }} asset_path: "c-artifacts/*.tar.gz" + # go + go-test: + name: Go Test - ${{ matrix.platform }} + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + # Windows x64 + - runner: windows-latest + target: x86_64-pc-windows-gnu + platform: windows-amd64 + # Linux x64 + - runner: ubuntu-latest + target: x86_64-unknown-linux-gnu + platform: linux-amd64 + # Linux ARM64 + - runner: ubuntu-latest + target: aarch64-unknown-linux-gnu + platform: linux-arm64 + # macOS ARM64 + - runner: macos-14 + target: aarch64-apple-darwin + platform: darwin-arm64 + # macOS x64 + - runner: macos-13 + target: x86_64-apple-darwin + platform: darwin-amd64 + steps: + # pull_request_target에서도 PR 코드를 체크아웃해 테스트한다. + - uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.target }} + + - name: Install cross-compilation tools (Linux ARM64) + if: matrix.platform == 'linux-arm64' + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu + + - uses: actions/setup-go@v5 + with: + go-version: stable + + - name: Build native library + run: cargo build --release --target ${{ matrix.target }} -p braillify-go + env: + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + + - name: Copy native library + run: | + mkdir -p packages/go/libs/${{ matrix.platform }} + cp target/${{ matrix.target }}/release/libbraillify_go.a packages/go/libs/${{ matrix.platform }}/ + shell: bash + + - name: Test + run: go test -v ./... + working-directory: packages/go + + go-build: + name: Go Build - ${{ matrix.platform }} + runs-on: ${{ matrix.runner }} + needs: + - test + - changepacks + if: >- + github.event_name == 'push' && + github.ref == 'refs/heads/main' && + contains(needs.changepacks.outputs.changepacks, 'packages/go/Cargo.toml') + strategy: + fail-fast: false + matrix: + include: + # Windows x64 + - runner: windows-latest + target: x86_64-pc-windows-gnu + platform: windows-amd64 + # Linux x64 + - runner: ubuntu-latest + target: x86_64-unknown-linux-gnu + platform: linux-amd64 + # Linux ARM64 + - runner: ubuntu-latest + target: aarch64-unknown-linux-gnu + platform: linux-arm64 + # macOS ARM64 + - runner: macos-14 + target: aarch64-apple-darwin + platform: darwin-arm64 + # macOS x64 + - runner: macos-13 + target: x86_64-apple-darwin + platform: darwin-amd64 + steps: + - uses: actions/checkout@v6 + + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.target }} + + - name: Install cross-compilation tools (Linux ARM64) + if: matrix.platform == 'linux-arm64' + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu + + - name: Build native library + run: cargo build --release --target ${{ matrix.target }} -p braillify-go + env: + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + + - name: Upload artifact + uses: actions/upload-artifact@v7 + with: + name: go-lib-${{ matrix.platform }} + path: target/${{ matrix.target }}/release/libbraillify_go.a + if-no-files-found: error + + go-publish: + name: Go Publish + runs-on: ubuntu-latest + needs: + - changepacks + - go-build + if: >- + github.event_name == 'push' && + github.ref == 'refs/heads/main' && + contains(needs.changepacks.outputs.changepacks, 'packages/go/Cargo.toml') + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Extract version + id: version + working-directory: packages/go + run: | + v=$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"([0-9.]+)".*/\1/') + echo "version=$v" >> $GITHUB_OUTPUT + echo "Resolved version: $v" + + - name: Download platform libs + uses: actions/download-artifact@v8 + with: + pattern: go-lib-* + path: libs-raw + merge-multiple: false + + - name: Place libs + run: | + for d in libs-raw/go-lib-*; do + [ -d "$d" ] || continue + plat="${d##*go-lib-}" + mkdir -p "packages/go/libs/$plat" + cp "$d/libbraillify_go.a" "packages/go/libs/$plat/" + done + echo "=== Placed libs ===" + find packages/go/libs -type f + + - name: Commit libs + new module tag + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + TAG="packages/go/v${{ steps.version.outputs.version }}" + if git rev-parse "$TAG" >/dev/null 2>&1; then + echo "Tag $TAG already exists — skip (no force-move)." + exit 0 + fi + git add -f packages/go/libs/ + git commit -m "chore(go): publish static libs v${{ steps.version.outputs.version }} [skip ci]" + git pull --rebase origin main + git tag "$TAG" + git push origin main + git push origin "$TAG" # jvm # JVM 릴리스 job은 다른 바인딩 게시 흐름과 섞이지 않도록 파일 맨 아래에 둔다. @@ -1069,19 +1255,23 @@ jobs: library-name: braillify_jvm.dll steps: - uses: actions/checkout@v7 + - name: Setup Rust uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: stable target: ${{ matrix.rust-target }} + - name: Set up MSVC if: runner.os == 'Windows' uses: ilammy/msvc-dev-cmd@v1 + - name: Set up Zig if: matrix.glibc uses: mlugg/setup-zig@v2 with: version: 0.14.1 + - name: Install cargo-zigbuild if: matrix.glibc uses: taiki-e/install-action@v2 @@ -1089,14 +1279,17 @@ jobs: GITHUB_TOKEN: ${{ github.token }} with: tool: cargo-zigbuild + - name: Build Linux native library with glibc 2.17 compatibility if: matrix.glibc run: cargo zigbuild --release -p braillify-jvm --target ${{ matrix.rust-target }}.${{ matrix.glibc }} + - name: Build macOS or Windows native library if: ${{ !matrix.glibc }} run: cargo build --release -p braillify-jvm --target ${{ matrix.rust-target }} env: MACOSX_DEPLOYMENT_TARGET: ${{ matrix.deployment-target }} + - name: Inspect Linux ABI and dependencies if: runner.os == 'Linux' shell: bash @@ -1115,12 +1308,15 @@ jobs: if [ "${{ matrix.native-target }}" = "linux-x86_64" ]; then ldd "$LIBRARY" fi + - name: Inspect macOS dependencies if: runner.os == 'macOS' run: otool -L target/${{ matrix.rust-target }}/release/${{ matrix.library-name }} + - name: Inspect Windows dependencies if: runner.os == 'Windows' run: dumpbin /DEPENDENTS target/${{ matrix.rust-target }}/release/${{ matrix.library-name }} + - name: Upload native library uses: actions/upload-artifact@v7 with: @@ -1138,20 +1334,25 @@ jobs: - jvm-native-build steps: - uses: actions/checkout@v7 + - name: Validate Gradle Wrapper uses: gradle/actions/wrapper-validation@v4 + - name: Set up JDK 17 uses: actions/setup-java@v5 with: distribution: temurin java-version: "17" + - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 + - name: Download native libraries uses: actions/download-artifact@v8 with: pattern: jvm-native-* path: native-artifacts + - name: Stage native libraries shell: bash run: | @@ -1174,9 +1375,11 @@ jobs: stage_native macos-x86_64 libbraillify_jvm.dylib stage_native macos-aarch64 libbraillify_jvm.dylib stage_native windows-x86_64 braillify_jvm.dll + - name: Assemble fat JAR run: ./gradlew assembleJvmJar --no-daemon working-directory: packages/jvm + - name: Verify fat JAR shell: bash run: | @@ -1202,6 +1405,7 @@ jobs: exit 1 fi sha256sum "$JAR" + - name: Upload fat JAR uses: actions/upload-artifact@v7 with: @@ -1256,11 +1460,13 @@ jobs: with: distribution: temurin java-version: ${{ matrix.java-version }} + - name: Download fat JAR uses: actions/download-artifact@v8 with: name: jvm-fat-jar path: jvm-dist + - name: Compile and run external consumer shell: bash run: | @@ -1292,6 +1498,7 @@ jobs: '}' > Consumer.java javac -cp "$JAR" Consumer.java java ${{ matrix.java-options }} -cp "$JAR${{ matrix.path-separator }}." Consumer + - name: Compile and run Kotlin consumer if: matrix.native-target == 'linux-x86_64' && matrix.java-version == '17' shell: bash @@ -1322,20 +1529,25 @@ jobs: attestations: write steps: - uses: actions/checkout@v7 + - name: Validate Gradle Wrapper uses: gradle/actions/wrapper-validation@v4 + - name: Set up JDK 17 uses: actions/setup-java@v5 with: distribution: temurin java-version: "17" + - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 + - name: Download native libraries uses: actions/download-artifact@v8 with: pattern: jvm-native-* path: native-artifacts + - name: Stage native libraries shell: bash run: | @@ -1358,6 +1570,7 @@ jobs: stage_native macos-x86_64 libbraillify_jvm.dylib stage_native macos-aarch64 libbraillify_jvm.dylib stage_native windows-x86_64 braillify_jvm.dll + - name: Check whether the JVM release already exists id: jvm-release shell: bash @@ -1375,6 +1588,7 @@ jobs: echo "exists=false" >> "$GITHUB_OUTPUT" fi working-directory: packages/jvm + - name: Create signed Maven Central bundle if: steps.jvm-release.outputs.exists != 'true' run: ./gradlew createCentralBundle --no-daemon @@ -1382,6 +1596,7 @@ jobs: env: MAVEN_SIGNING_KEY: ${{ secrets.MAVEN_SIGNING_KEY }} MAVEN_SIGNING_PASSWORD: ${{ secrets.MAVEN_SIGNING_PASSWORD }} + - name: Verify Maven Central bundle if: steps.jvm-release.outputs.exists != 'true' shell: bash @@ -1396,11 +1611,13 @@ jobs: } done sha256sum packages/jvm/build/central/central-bundle.zip + - name: Generate artifact attestation if: steps.jvm-release.outputs.exists != 'true' uses: actions/attest-build-provenance@v4 with: subject-path: packages/jvm/build/central/central-bundle.zip + - name: Publish and validate on Maven Central Portal if: steps.jvm-release.outputs.exists != 'true' run: ./gradlew publishToCentralPortal --no-daemon @@ -1410,6 +1627,7 @@ jobs: MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} MAVEN_SIGNING_KEY: ${{ secrets.MAVEN_SIGNING_KEY }} MAVEN_SIGNING_PASSWORD: ${{ secrets.MAVEN_SIGNING_PASSWORD }} + - name: Upload Central bundle if: steps.jvm-release.outputs.exists != 'true' uses: actions/upload-artifact@v7 @@ -1417,4 +1635,4 @@ jobs: name: jvm-central-bundle path: packages/jvm/build/central/central-bundle.zip if-no-files-found: error - retention-days: 7 + retention-days: 7 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index c3c867c3..33454873 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -199,6 +199,13 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "braillify-go" +version = "2.0.0" +dependencies = [ + "braillify", +] + [[package]] name = "braillify-c" version = "0.1.2" diff --git a/packages/go/.gitignore b/packages/go/.gitignore new file mode 100644 index 00000000..46e1819d --- /dev/null +++ b/packages/go/.gitignore @@ -0,0 +1,4 @@ +*.test +*.exe +*.out +libs/ diff --git a/packages/go/Cargo.toml b/packages/go/Cargo.toml new file mode 100644 index 00000000..1a17dae8 --- /dev/null +++ b/packages/go/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "braillify-go" +version = "2.0.0" +edition = "2024" + +[lib] +name = "braillify_go" +crate-type = ["cdylib", "staticlib"] + +[dependencies] +braillify = { path = "../../libs/braillify", default-features = false } diff --git a/packages/go/braillify.go b/packages/go/braillify.go new file mode 100644 index 00000000..597e1d25 --- /dev/null +++ b/packages/go/braillify.go @@ -0,0 +1,16 @@ +package braillify + +// Encode converts Korean text to braille byte representation. +func Encode(text string) ([]byte, error) { + return cEncode(text) +} + +// EncodeToUnicode converts Korean text to braille Unicode string. +func EncodeToUnicode(text string) (string, error) { + return cEncodeToUnicode(text) +} + +// EncodeToBrailleFont converts Korean text to braille font string. +func EncodeToBrailleFont(text string) (string, error) { + return cEncodeToBrailleFont(text) +} diff --git a/packages/go/braillify_test.go b/packages/go/braillify_test.go new file mode 100644 index 00000000..e12bdfab --- /dev/null +++ b/packages/go/braillify_test.go @@ -0,0 +1,51 @@ +package braillify + +import "testing" + +func TestEncodeToUnicode(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"안녕하세요", "⠣⠒⠉⠻⠚⠠⠝⠬"}, + {"상상이상의", "⠇⠶⠇⠶⠕⠇⠶⠺"}, + {"1,000", "⠼⠁⠂⠚⠚⠚"}, + {"ATM", "⠠⠠⠁⠞⠍"}, + {"", ""}, + } + + for _, tt := range tests { + result, err := EncodeToUnicode(tt.input) + if err != nil { + t.Errorf("EncodeToUnicode(%q): unexpected error: %v", tt.input, err) + continue + } + t.Logf("EncodeToUnicode(%q) = %q", tt.input, result) + if result != tt.expected { + t.Errorf("EncodeToUnicode(%q) = %q, want %q", tt.input, result, tt.expected) + } + } +} + +func TestEncode(t *testing.T) { + result, err := Encode("안녕") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + t.Logf("Encode(%q) = %v", "안녕", result) + if len(result) == 0 { + t.Error("expected non-empty byte slice") + } +} + +func TestEncodeToBrailleFont(t *testing.T) { + result, err := EncodeToBrailleFont("안녕하세요") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expected := "⠣⠒⠉⠻⠚⠠⠝⠬" + t.Logf("EncodeToBrailleFont(%q) = %q", "안녕하세요", result) + if result != expected { + t.Errorf("EncodeToBrailleFont = %q, want %q", result, expected) + } +} diff --git a/packages/go/cgo.go b/packages/go/cgo.go new file mode 100644 index 00000000..3048345d --- /dev/null +++ b/packages/go/cgo.go @@ -0,0 +1,85 @@ +package braillify + +/* +#cgo darwin,amd64 LDFLAGS: -L${SRCDIR}/libs/darwin-amd64 -lbraillify_go -lm -lpthread +#cgo darwin,arm64 LDFLAGS: -L${SRCDIR}/libs/darwin-arm64 -lbraillify_go -lm -lpthread +#cgo linux,amd64 LDFLAGS: -L${SRCDIR}/libs/linux-amd64 -lbraillify_go -lm -lpthread -ldl +#cgo linux,arm64 LDFLAGS: -L${SRCDIR}/libs/linux-arm64 -lbraillify_go -lm -lpthread -ldl +#cgo windows,amd64 LDFLAGS: -L${SRCDIR}/libs/windows-amd64 -lbraillify_go -lntdll -lws2_32 -lbcrypt -ladvapi32 -luserenv + +#include +#include +#include + +extern uint8_t* braillify_encode(const char* text, size_t* out_len); +extern char* braillify_encode_to_unicode(const char* text); +extern char* braillify_encode_to_braille_font(const char* text); +extern char* braillify_get_last_error(); +extern void braillify_free_string(char* ptr); +extern void braillify_free_bytes(uint8_t* ptr, size_t len); +*/ +import "C" + +import ( + "errors" + "runtime" + "unsafe" +) + +func cEncode(text string) ([]byte, error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + cText := C.CString(text) + defer C.free(unsafe.Pointer(cText)) + + var outLen C.size_t + result := C.braillify_encode(cText, &outLen) + if result == nil { + return nil, getLastError() + } + defer C.braillify_free_bytes(result, outLen) + + return C.GoBytes(unsafe.Pointer(result), C.int(outLen)), nil +} + +func cEncodeToUnicode(text string) (string, error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + cText := C.CString(text) + defer C.free(unsafe.Pointer(cText)) + + result := C.braillify_encode_to_unicode(cText) + if result == nil { + return "", getLastError() + } + defer C.braillify_free_string(result) + + return C.GoString(result), nil +} + +func cEncodeToBrailleFont(text string) (string, error) { + runtime.LockOSThread() + defer runtime.UnlockOSThread() + + cText := C.CString(text) + defer C.free(unsafe.Pointer(cText)) + + result := C.braillify_encode_to_braille_font(cText) + if result == nil { + return "", getLastError() + } + defer C.braillify_free_string(result) + + return C.GoString(result), nil +} + +func getLastError() error { + errPtr := C.braillify_get_last_error() + if errPtr == nil { + return errors.New("braillify: unknown error") + } + defer C.braillify_free_string(errPtr) + return errors.New(C.GoString(errPtr)) +} diff --git a/packages/go/go.mod b/packages/go/go.mod new file mode 100644 index 00000000..d54c11dd --- /dev/null +++ b/packages/go/go.mod @@ -0,0 +1,3 @@ +module github.com/dev-five-git/braillify/packages/go + +go 1.21 diff --git a/packages/go/src/lib.rs b/packages/go/src/lib.rs new file mode 100644 index 00000000..dae73c3a --- /dev/null +++ b/packages/go/src/lib.rs @@ -0,0 +1,402 @@ +use std::cell::RefCell; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; +use std::ptr; + +thread_local! { + static LAST_ERROR: RefCell> = const { RefCell::new(None) }; +} + +fn set_last_error(err: String) { + LAST_ERROR.with(|e| { + *e.borrow_mut() = Some(err); + }); +} + +fn clear_last_error() { + LAST_ERROR.with(|e| { + *e.borrow_mut() = None; + }); +} + +fn string_into_c_ptr(value: String) -> *mut c_char { + match CString::new(value) { + Ok(c_string) => c_string.into_raw(), + Err(e) => { + set_last_error(format!("CString conversion error: {}", e)); + ptr::null_mut() + } + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn braillify_get_last_error() -> *mut c_char { + LAST_ERROR.with(|e| match e.borrow().as_ref() { + // Error messages never contain null bytes, so unwrap_or(null) + // is defensive dead code. + Some(msg) => CString::new(msg.clone()) + .map(|s| s.into_raw()) + .unwrap_or(ptr::null_mut()), + None => ptr::null_mut(), + }) +} + +/// Encodes a null-terminated UTF-8 C string into Braille bytes. +/// +/// On success, the output length is written to `out_len`. +/// The returned buffer must be released using [`braillify_free_bytes`]. +/// +/// # Safety +/// +/// - `text` must be null or point to a valid null-terminated C string. +/// - `out_len` must be null or point to valid writable memory for a `usize`. +/// - A non-null returned pointer must be freed exactly once using +/// [`braillify_free_bytes`] with the length written to `out_len`. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn braillify_encode(text: *const c_char, out_len: *mut usize) -> *mut u8 { + clear_last_error(); + + if text.is_null() || out_len.is_null() { + set_last_error("Null pointer argument".to_string()); + return ptr::null_mut(); + } + + let c_str = unsafe { CStr::from_ptr(text) }; + let text_str = match c_str.to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(format!("Invalid UTF-8: {}", e)); + return ptr::null_mut(); + } + }; + + match braillify::encode(text_str) { + Ok(result) => { + unsafe { + *out_len = result.len(); + } + + let boxed = result.into_boxed_slice(); + Box::into_raw(boxed) as *mut u8 + } + Err(e) => { + set_last_error(e); + ptr::null_mut() + } + } +} + +/// Encodes a null-terminated UTF-8 C string into Unicode Braille. +/// +/// The returned string must be released using [`braillify_free_string`]. +/// +/// # Safety +/// +/// - `text` must be null or point to a valid null-terminated C string. +/// - A non-null returned pointer must be freed exactly once using +/// [`braillify_free_string`]. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn braillify_encode_to_unicode(text: *const c_char) -> *mut c_char { + clear_last_error(); + + if text.is_null() { + set_last_error("Null pointer argument".to_string()); + return ptr::null_mut(); + } + + let c_str = unsafe { CStr::from_ptr(text) }; + let text_str = match c_str.to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(format!("Invalid UTF-8: {}", e)); + return ptr::null_mut(); + } + }; + + match braillify::encode_to_unicode(text_str) { + Ok(result) => string_into_c_ptr(result), + Err(e) => { + set_last_error(e); + ptr::null_mut() + } + } +} + +/// Encodes a null-terminated UTF-8 C string for use with a Braille font. +/// +/// The returned string must be released using [`braillify_free_string`]. +/// +/// # Safety +/// +/// - `text` must be null or point to a valid null-terminated C string. +/// - A non-null returned pointer must be freed exactly once using +/// [`braillify_free_string`]. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn braillify_encode_to_braille_font(text: *const c_char) -> *mut c_char { + clear_last_error(); + + if text.is_null() { + set_last_error("Null pointer argument".to_string()); + return ptr::null_mut(); + } + + let c_str = unsafe { CStr::from_ptr(text) }; + let text_str = match c_str.to_str() { + Ok(s) => s, + Err(e) => { + set_last_error(format!("Invalid UTF-8: {}", e)); + return ptr::null_mut(); + } + }; + + match braillify::encode_to_braille_font(text_str) { + Ok(result) => string_into_c_ptr(result), + Err(e) => { + set_last_error(e); + ptr::null_mut() + } + } +} + +/// Frees a C string returned by this library. +/// +/// # Safety +/// +/// - `ptr` must be null or a pointer previously returned by a string-producing +/// function in this library. +/// - The pointer must not have already been freed. +/// - The pointer must not be used after this function returns. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn braillify_free_string(ptr: *mut c_char) { + if !ptr.is_null() { + unsafe { + drop(CString::from_raw(ptr)); + } + } +} + +/// Frees a byte buffer returned by [`braillify_encode`]. +/// +/// # Safety +/// +/// - `ptr` must be null or a pointer previously returned by +/// [`braillify_encode`]. +/// - `len` must be the exact length written to `out_len` by +/// [`braillify_encode`]. +/// - The buffer must not have already been freed. +/// - The pointer must not be used after this function returns. +#[unsafe(no_mangle)] +pub unsafe extern "C" fn braillify_free_bytes(ptr: *mut u8, len: usize) { + if !ptr.is_null() { + unsafe { + let _ = Vec::from_raw_parts(ptr, len, len); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::ffi::CString; + use std::ptr; + + #[test] + fn string_into_c_ptr_converts_valid_string() { + let result = string_into_c_ptr("braille".to_string()); + + assert!(!result.is_null()); + let c_string = unsafe { CString::from_raw(result) }; + assert_eq!(c_string.to_str().unwrap(), "braille"); + } + + #[test] + fn string_into_c_ptr_rejects_interior_null() { + clear_last_error(); + + let result = string_into_c_ptr("braille\0output".to_string()); + + assert!(result.is_null()); + let error = braillify_get_last_error(); + assert!(!error.is_null()); + let error = unsafe { CString::from_raw(error) }; + assert!(error.to_str().unwrap().contains("CString conversion error")); + } + + #[test] + fn test_encode_to_unicode() { + let input = CString::new("안녕하세요").unwrap(); + let result = unsafe { braillify_encode_to_unicode(input.as_ptr()) }; + + assert!(!result.is_null()); + + let c_str = unsafe { CString::from_raw(result) }; + assert_eq!(c_str.to_str().unwrap(), "⠣⠒⠉⠻⠚⠠⠝⠬"); + } + + #[test] + fn test_encode_to_unicode_empty() { + let input = CString::new("").unwrap(); + let result = unsafe { braillify_encode_to_unicode(input.as_ptr()) }; + + assert!(!result.is_null()); + + let c_str = unsafe { CString::from_raw(result) }; + assert_eq!(c_str.to_str().unwrap(), ""); + } + + #[test] + fn test_encode_to_unicode_null() { + let result = unsafe { braillify_encode_to_unicode(ptr::null()) }; + assert!(result.is_null()); + } + + #[test] + fn test_encode_to_braille_font() { + let input = CString::new("안녕하세요").unwrap(); + let result = unsafe { braillify_encode_to_braille_font(input.as_ptr()) }; + + assert!(!result.is_null()); + + let c_str = unsafe { CString::from_raw(result) }; + assert_eq!(c_str.to_str().unwrap(), "⠣⠒⠉⠻⠚⠠⠝⠬"); + } + + #[test] + fn test_encode_to_braille_font_null() { + let result = unsafe { braillify_encode_to_braille_font(ptr::null()) }; + assert!(result.is_null()); + } + + #[test] + fn test_encode() { + let input = CString::new("안녕").unwrap(); + let mut out_len: usize = 0; + + let result = unsafe { braillify_encode(input.as_ptr(), &mut out_len) }; + + assert!(!result.is_null()); + assert!(out_len > 0); + + unsafe { + braillify_free_bytes(result, out_len); + } + } + + #[test] + fn test_encode_null_text() { + let mut out_len: usize = 0; + let result = unsafe { braillify_encode(ptr::null(), &mut out_len) }; + + assert!(result.is_null()); + } + + #[test] + fn test_encode_null_out_len() { + let input = CString::new("test").unwrap(); + let result = unsafe { braillify_encode(input.as_ptr(), ptr::null_mut()) }; + + assert!(result.is_null()); + } + + #[test] + fn test_get_last_error_after_null() { + let _ = unsafe { braillify_encode_to_unicode(ptr::null()) }; + let err = braillify_get_last_error(); + + assert!(!err.is_null()); + + let err_str = unsafe { CString::from_raw(err) }; + assert!(err_str.to_str().unwrap().contains("Null pointer")); + } + + #[test] + fn test_free_string_null() { + unsafe { + braillify_free_string(ptr::null_mut()); + } + } + + #[test] + fn test_free_bytes_null() { + unsafe { + braillify_free_bytes(ptr::null_mut(), 0); + } + } + + #[test] + fn test_encode_invalid_utf8() { + let input = unsafe { CString::from_vec_unchecked(vec![0xFF, 0xFE]) }; + let mut out_len: usize = 0; + + let result = unsafe { braillify_encode(input.as_ptr(), &mut out_len) }; + + assert!(result.is_null()); + } + + #[test] + fn test_encode_to_unicode_invalid_utf8() { + let input = unsafe { CString::from_vec_unchecked(vec![0xFF, 0xFE]) }; + let result = unsafe { braillify_encode_to_unicode(input.as_ptr()) }; + + assert!(result.is_null()); + } + + #[test] + fn test_encode_to_braille_font_invalid_utf8() { + let input = unsafe { CString::from_vec_unchecked(vec![0xFF, 0xFE]) }; + let result = unsafe { braillify_encode_to_braille_font(input.as_ptr()) }; + + assert!(result.is_null()); + } + + #[test] + fn test_free_string_non_null() { + let string = CString::new("test").unwrap(); + let ptr = string.into_raw(); + + unsafe { + braillify_free_string(ptr); + } + } + + #[test] + fn test_get_last_error_none() { + let input = CString::new("a").unwrap(); + let result = unsafe { braillify_encode_to_unicode(input.as_ptr()) }; + + assert!(!result.is_null()); + + unsafe { + braillify_free_string(result); + } + + let err = braillify_get_last_error(); + assert!(err.is_null()); + } + + #[test] + fn test_encode_invalid_char() { + let input = CString::new("😀").unwrap(); + let mut out_len: usize = 0; + + let result = unsafe { braillify_encode(input.as_ptr(), &mut out_len) }; + + assert!(result.is_null()); + } + + #[test] + fn test_encode_to_unicode_invalid_char() { + let input = CString::new("😀").unwrap(); + let result = unsafe { braillify_encode_to_unicode(input.as_ptr()) }; + + assert!(result.is_null()); + } + + #[test] + fn test_encode_to_braille_font_invalid_char() { + let input = CString::new("😀").unwrap(); + let result = unsafe { braillify_encode_to_braille_font(input.as_ptr()) }; + + assert!(result.is_null()); + } +}