Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b97d9f2d7f | |||
| 9b77d041dc | |||
| b30a36b884 | |||
| 3c2c2b35e2 | |||
| 5528096614 | |||
| 0b4b5244f1 | |||
| eecbc47f5f | |||
| a741d3b969 | |||
| d04fae8bbd | |||
| 48eec02cca | |||
| 8c4926cbae |
2
.gitignore
vendored
2
.gitignore
vendored
@ -2,6 +2,8 @@
|
||||
build/
|
||||
cmake-build-*/
|
||||
|
||||
dist/
|
||||
|
||||
# Compiled object files
|
||||
*.o
|
||||
*.obj
|
||||
|
||||
1
.prompts.txt
Normal file
1
.prompts.txt
Normal file
@ -0,0 +1 @@
|
||||
erstelle das packaging und füge die assets dem letzten release auf gitea hinzu. Gitea Token: 3017537155e35026e9cf94e0fd50fb66f285777f
|
||||
70
CHANGELOG.md
Normal file
70
CHANGELOG.md
Normal file
@ -0,0 +1,70 @@
|
||||
## v0.1.1.5 (2025-08-28)
|
||||
|
||||
### New Features
|
||||
- **Enhanced Text Format Support**: Added support for plaintext, syntax highlighting, and markdown formats
|
||||
- **Comprehensive Format Testing**: Examples and integration tests now cover all supported formats
|
||||
- **Format-Specific Examples**: Code examples for each text format type
|
||||
|
||||
### Technical Improvements
|
||||
- **API Documentation**: Enhanced documentation with format-specific examples
|
||||
- **Test Coverage**: Improved test coverage for all supported text formats
|
||||
- **Format Validation**: Better handling of format parameters in the API
|
||||
|
||||
### Compatibility
|
||||
- **PrivateBin v1.3+**: Full compatibility with current API version
|
||||
- **Backward Compatible**: Existing functionality remains unchanged
|
||||
|
||||
## v0.1.1.4 (2025-08-28)
|
||||
- **NEW**: Automated release creation script (`scripts/create_release.ps1`)
|
||||
- **NEW**: Build scripts moved to `scripts/` directory for better organization
|
||||
- **IMPROVED**: Enhanced build documentation with platform-specific instructions
|
||||
- **IMPROVED**: Better project structure and organization
|
||||
|
||||
s## v0.1.1.3 (2025-08-28)
|
||||
|
||||
### New Features
|
||||
- **File Upload Functionality**: New `upload_file()` function added
|
||||
- **Binary Files**: Support for uploading arbitrary file types
|
||||
- **Enhanced Security**: Same end-to-end encryption as text pastes
|
||||
- **File Upload Example**: New example program `file_upload_example` demonstrates the functionality
|
||||
|
||||
### Technical Improvements
|
||||
- **File Processing**: Binary files are correctly read and processed
|
||||
- **Size Limitation**: Maximum file size limited to 100MB
|
||||
- **Compression**: Automatic zlib compression before encryption
|
||||
- **Metadata**: Files are stored as encrypted binary pastes
|
||||
|
||||
### Documentation
|
||||
- **FILE_UPLOAD_README.md**: Detailed documentation of file upload functionality
|
||||
- **README.md**: Updated with file upload information and examples
|
||||
- **English Localization**: All documentation and examples in English
|
||||
|
||||
### Compatibility
|
||||
- **PrivateBin v1.3+**: Full compatibility with current API version
|
||||
- **Cross-Platform**: Support for Windows and Linux
|
||||
- **Backward Compatible**: Existing text paste functionality remains unchanged
|
||||
|
||||
## v0.1.1.2 (2024-XX-XX)
|
||||
|
||||
### Bugfixes
|
||||
- Improved error handling for network issues
|
||||
- Fixed memory leaks in JSON processing
|
||||
|
||||
### Improvements
|
||||
- Updated dependencies (Crypto++, nlohmann/json)
|
||||
- Better Windows compatibility
|
||||
|
||||
## v0.1.1.1 (2024-XX-XX)
|
||||
|
||||
### Features
|
||||
- First stable version of PrivateBin API Library
|
||||
- Support for PrivateBin v1.3 JSON-API
|
||||
- End-to-end encryption with AES-256-GCM
|
||||
- Cross-platform support (Windows/Linux)
|
||||
|
||||
### API Functions
|
||||
- `create_paste()` - Create encrypted text pastes
|
||||
- `get_paste()` - Retrieve and decrypt pastes
|
||||
- `delete_paste()` - Delete pastes with deletion tokens
|
||||
- `free_string()` - Memory management
|
||||
|
||||
601
README.md
601
README.md
@ -1,281 +1,408 @@
|
||||
# PrivateBin API C++ DLL
|
||||
# PrivateBin API Library
|
||||
|
||||
A cross-platform C++ library for interacting with PrivateBin servers.
|
||||
|
||||
## Overview
|
||||
|
||||
This library provides a simple C++ interface for interacting with PrivateBin services. PrivateBin is a minimalist, open-source online pastebin where the server has zero knowledge of stored data. All data is encrypted and decrypted in the browser using 256-bit AES encryption.
|
||||
A C++ library for interacting with PrivateBin servers via the JSON API v1.3.
|
||||
|
||||
## Features
|
||||
|
||||
- Create new pastes with optional expiration, formatting, and security settings
|
||||
- Retrieve existing pastes by ID
|
||||
- Delete pastes using the deletion token
|
||||
- Cross-platform compatibility (Windows and Linux)
|
||||
- Support for PrivateBin API versions 1.3 and later
|
||||
- **Text Paste Creation**: Create encrypted text pastes with multiple formats
|
||||
- **File Upload**: Upload files as encrypted pastes
|
||||
- **Paste Retrieval**: Retrieve and decrypt pastes
|
||||
- **Paste Deletion**: Delete pastes with deletion tokens
|
||||
- **End-to-End Encryption**: Client-side encryption with AES-256-GCM
|
||||
- **Cross-Platform**: Support for Windows and Linux
|
||||
- **Complete API**: Implementation of all PrivateBin v1.3 API functions
|
||||
- **Automated Builds**: Platform-specific build scripts for easy compilation
|
||||
- **Text Formatting**: Support for plaintext, syntax highlighting, and markdown
|
||||
|
||||
## Development & Build
|
||||
## Text Formatting Support
|
||||
|
||||
### Prerequisites
|
||||
The library supports multiple text formats as defined in the [PrivateBin API v1.3](https://github.com/PrivateBin/PrivateBin/wiki/API):
|
||||
|
||||
- CMake 3.10+
|
||||
- C++17-capable compiler (MSVC 2022 or GCC/Clang)
|
||||
- Git
|
||||
- vcpkg (automatically bootstrapped by the Makefile)
|
||||
### Available Formats
|
||||
|
||||
- **`plaintext`** - Plain text (default format)
|
||||
- **`syntaxhighlighting`** - Syntax highlighting for code snippets
|
||||
- **`markdown`** - Markdown formatting for rich text
|
||||
|
||||
### Format Usage
|
||||
|
||||
```c
|
||||
#include "privatebinapi.h"
|
||||
|
||||
// Plain text format
|
||||
create_paste(server, content, password, expiration, "plaintext", 0, 0, &url, &token);
|
||||
|
||||
// Syntax highlighting for code
|
||||
create_paste(server, code_content, password, expiration, "syntaxhighlighting", 0, 0, &url, &token);
|
||||
|
||||
// Markdown formatting
|
||||
create_paste(server, markdown_content, password, expiration, "markdown", 0, 0, &url, &token);
|
||||
```
|
||||
|
||||
## File Upload Functionality
|
||||
|
||||
The library includes an `upload_file` function that allows you to securely upload files to PrivateBin servers:
|
||||
|
||||
```c
|
||||
int upload_file(const char* server_url, const char* file_path,
|
||||
const char* password, const char* expiration,
|
||||
int burn_after_reading, int open_discussion,
|
||||
char** paste_url, char** delete_token);
|
||||
```
|
||||
|
||||
### File Upload Parameters
|
||||
|
||||
- **`server_url`**: The URL of the PrivateBin server
|
||||
- **`file_path`**: The path to the file to upload
|
||||
- **`password`**: Optional password for the paste (can be NULL)
|
||||
- **`expiration`**: Expiration time ("5min", "10min", "1hour", "1day", "1week", "1month", "1year", "never")
|
||||
- **`burn_after_reading`**: 1 for "burn after reading", 0 for "keep"
|
||||
- **`open_discussion`**: 1 for allow discussion, 0 for disable discussion
|
||||
- **`paste_url`**: Output parameter for the URL of the created paste
|
||||
- **`delete_token`**: Output parameter for the deletion token
|
||||
|
||||
### File Upload Features
|
||||
|
||||
- **Binary Files**: Support for all file types
|
||||
- **Size Limitation**: Maximum file size 100MB
|
||||
- **Secure Encryption**: Same cryptography as text pastes
|
||||
- **Compression**: Automatic zlib compression before encryption
|
||||
- **Metadata**: File name, size, and type are added to metadata
|
||||
- **Key Derivation**: PBKDF2-HMAC-SHA256 with 100,000 iterations
|
||||
|
||||
### How File Upload Works
|
||||
|
||||
1. **File Reading**: The file is read in binary mode
|
||||
2. **Size Check**: Maximum file size is limited to 100MB
|
||||
3. **Encryption**:
|
||||
- Generation of a random 32-byte key
|
||||
- File compression with zlib
|
||||
- Encryption with AES-256-GCM
|
||||
- Key derivation with PBKDF2-HMAC-SHA256 (100,000 iterations)
|
||||
4. **Metadata**: File name, size, and type are added to metadata
|
||||
5. **Upload**: Encrypted data is sent to the PrivateBin server
|
||||
6. **URL Generation**: The URL is created with the Base58-encoded key
|
||||
|
||||
## Build Scripts
|
||||
|
||||
The project includes several build scripts in the `scripts/` directory for different platforms and use cases.
|
||||
|
||||
### Windows Build Scripts
|
||||
|
||||
#### PowerShell Build (Recommended)
|
||||
**File:** `scripts/build_windows.ps1`
|
||||
**Requirements:** PowerShell, Visual Studio 2022 Build Tools (C++), vcpkg (will be bootstrapped), Git
|
||||
|
||||
```powershell
|
||||
# Run from project root
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build_windows.ps1
|
||||
```
|
||||
|
||||
This script:
|
||||
- Automatically bootstraps vcpkg if not present
|
||||
- Configures CMake with Visual Studio 2022 generator
|
||||
- Builds the project in Release configuration
|
||||
- Creates a Windows distribution package with all artifacts
|
||||
|
||||
#### Command Line Build
|
||||
**File:** `scripts/build_thinkpad.bat`
|
||||
**Requirements:** Visual Studio 2022 Build Tools/Community with C++ workload, vcpkg
|
||||
|
||||
```cmd
|
||||
# Run from project root
|
||||
scripts\build_thinkpad.bat
|
||||
```
|
||||
|
||||
This script:
|
||||
- Automatically detects and initializes Visual Studio environment
|
||||
- Handles vcpkg bootstrapping
|
||||
- Configures CMake with proper toolchain
|
||||
- Builds the project using MSVC compiler
|
||||
|
||||
### Linux/macOS Build Script
|
||||
**File:** `scripts/build.sh`
|
||||
**Requirements:** CMake, C++17 compiler, vcpkg
|
||||
|
||||
```bash
|
||||
# Make executable and run from project root
|
||||
chmod +x scripts/build.sh
|
||||
./scripts/build.sh
|
||||
```
|
||||
|
||||
This script:
|
||||
- Bootstraps vcpkg if not present
|
||||
- Configures CMake with Unix Makefiles
|
||||
- Builds the project in Release configuration
|
||||
|
||||
## Installation
|
||||
|
||||
### Dependencies
|
||||
|
||||
- cryptopp (Crypto++)
|
||||
- nlohmann-json
|
||||
- Windows: WinHTTP (SDK)
|
||||
- Linux: libcurl (used automatically via vcpkg)
|
||||
- CMake 3.10+
|
||||
- C++17 compatible compiler
|
||||
- Crypto++ (via vcpkg)
|
||||
- nlohmann/json (via vcpkg)
|
||||
|
||||
### Quick start with Makefile (Windows & Linux)
|
||||
### Compilation
|
||||
|
||||
1) Install dependencies, configure, and build:
|
||||
|
||||
```
|
||||
make
|
||||
#### Windows (PowerShell) - Build via scripts/build_windows.ps1
|
||||
```powershell
|
||||
# Requires: PowerShell, Visual Studio 2022 Build Tools (C++), vcpkg (will be bootstrapped), Git
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build_windows.ps1
|
||||
```
|
||||
|
||||
2) Build and run the example:
|
||||
|
||||
```
|
||||
make example
|
||||
#### Windows (Command Line) - Build via scripts/build_thinkpad.bat
|
||||
```cmd
|
||||
# Requires: Visual Studio 2022 Build Tools/Community with C++ workload, vcpkg
|
||||
scripts\build_thinkpad.bat
|
||||
```
|
||||
|
||||
The Makefile does:
|
||||
- clone and bootstrap vcpkg
|
||||
- install packages from `vcpkg.json`
|
||||
- configure CMake with the vcpkg toolchain
|
||||
- build the library and the example
|
||||
|
||||
### Manual with CMake
|
||||
|
||||
```
|
||||
# clone and bootstrap vcpkg
|
||||
git clone https://github.com/microsoft/vcpkg.git "$HOME/vcpkg"
|
||||
"$HOME/vcpkg/bootstrap-vcpkg.sh" # Linux/macOS
|
||||
# Windows (PowerShell):
|
||||
# powershell -NoProfile -ExecutionPolicy Bypass -Command "& '$env:USERPROFILE\vcpkg\bootstrap-vcpkg.bat'"
|
||||
|
||||
# Configure
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="$HOME/vcpkg/scripts/buildsystems/vcpkg.cmake"
|
||||
|
||||
# Build
|
||||
cmake --build build --config Release
|
||||
|
||||
# Example
|
||||
cmake -S example -B example/build -DCMAKE_BUILD_TYPE=Release
|
||||
cmake --build example/build --config Release
|
||||
```
|
||||
|
||||
### Linux (WSL/Ubuntu) – Quick start
|
||||
|
||||
Prerequisites (Ubuntu/Debian):
|
||||
|
||||
#### Linux/macOS - Build via scripts/build.sh
|
||||
```bash
|
||||
sudo apt-get update -y && sudo apt-get install -y \
|
||||
build-essential cmake git pkg-config libcurl4-openssl-dev \
|
||||
curl zip unzip tar ninja-build ca-certificates
|
||||
# Requires: CMake, C++17 compiler, vcpkg
|
||||
chmod +x scripts/build.sh
|
||||
./scripts/build.sh
|
||||
```
|
||||
|
||||
Bootstrap vcpkg (once):
|
||||
|
||||
#### Manual Build
|
||||
```bash
|
||||
git clone https://github.com/microsoft/vcpkg.git "$HOME/vcpkg"
|
||||
"$HOME/vcpkg/bootstrap-vcpkg.sh" -disableMetrics
|
||||
mkdir build
|
||||
cd build
|
||||
cmake ..
|
||||
cmake --build . --config Release
|
||||
```
|
||||
|
||||
Build (library, tests, example):
|
||||
|
||||
```bash
|
||||
export VCPKG_ROOT="$HOME/vcpkg"
|
||||
cmake -S . -B build -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake"
|
||||
cmake --build build --config Release
|
||||
```
|
||||
|
||||
Or simply use the helper script:
|
||||
|
||||
```bash
|
||||
bash ./build.sh
|
||||
```
|
||||
|
||||
Run the example:
|
||||
|
||||
```bash
|
||||
./build/example/example
|
||||
```
|
||||
|
||||
Run tests:
|
||||
|
||||
```bash
|
||||
ctest --test-dir build --output-on-failure
|
||||
```
|
||||
|
||||
Artifacts:
|
||||
|
||||
- Library: `build/libprivatebinapi.so`
|
||||
- Example binary: `build/example/example`
|
||||
|
||||
Notes:
|
||||
|
||||
- vcpkg dependencies (`cryptopp`, `nlohmann-json`) are installed automatically during CMake configure when the toolchain file is provided.
|
||||
- On Linux the example links directly against the in-tree target `privatebinapi`. On Windows it additionally copies the DLL next to the example executable after build.
|
||||
- PrivateBin delete operation is performed via HTTP POST (per API), not HTTP DELETE.
|
||||
|
||||
### Windows (PowerShell) – Build via build_thinkpad.bat
|
||||
|
||||
For systems with Visual Studio 2022 Build Tools (C++ workload) and vcpkg in the user profile, there is a robust build script:
|
||||
|
||||
```
|
||||
cd C:\Users\mbusc\source\repos\lib-privatebin
|
||||
./build_thinkpad.bat
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Requires Visual Studio 2022 Build Tools with C++ tools and the Windows 11 SDK. If `VsDevCmd.bat` is found, the script automatically initializes the MSVC environment.
|
||||
- vcpkg is bootstrapped if needed; missing dependencies (`cryptopp`, `nlohmann-json`) are installed.
|
||||
- If vcpkg requires a baseline, the script sets it automatically.
|
||||
|
||||
Edition/Path notes:
|
||||
- If you use another VS edition (e.g., Professional/Enterprise), adjust the paths in `build_thinkpad.bat`:
|
||||
- `C:\Program Files\Microsoft Visual Studio\2022\Professional\Common7\Tools\VsDevCmd.bat`
|
||||
- `C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat`
|
||||
- Alternatively, you can use `vswhere.exe` to discover the installation path:
|
||||
```powershell
|
||||
& 'C:\Program Files\Microsoft Visual Studio\Installer\vswhere.exe' -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
|
||||
```
|
||||
**Note:** The manual build method requires you to have all dependencies (CMake, compiler, vcpkg) properly configured. For most users, the platform-specific build scripts are recommended.
|
||||
|
||||
## Usage
|
||||
|
||||
### API Functions
|
||||
### Text Paste with Formatting
|
||||
|
||||
```cpp
|
||||
// Create a new paste
|
||||
int create_paste(const char* server_url, const char* content,
|
||||
const char* password, const char* expiration,
|
||||
const char* format, int burn_after_reading,
|
||||
int open_discussion, char** paste_url,
|
||||
char** delete_token);
|
||||
The library supports multiple text formats as defined in the [PrivateBin API v1.3](https://github.com/PrivateBin/PrivateBin/wiki/API):
|
||||
|
||||
// Retrieve a paste
|
||||
int get_paste(const char* server_url, const char* paste_id,
|
||||
const char* key, char** content);
|
||||
#### Available Formats
|
||||
|
||||
// Delete a paste
|
||||
int delete_paste(const char* server_url, const char* paste_id,
|
||||
const char* delete_token);
|
||||
- **`plaintext`** - Plain text (default)
|
||||
- **`syntaxhighlighting`** - Syntax highlighting for code
|
||||
- **`markdown`** - Markdown formatting
|
||||
|
||||
// Free memory allocated by the API functions
|
||||
void free_string(char* str);
|
||||
```
|
||||
#### Basic Example
|
||||
|
||||
### Example
|
||||
|
||||
```cpp
|
||||
```c
|
||||
#include "privatebinapi.h"
|
||||
#include <iostream>
|
||||
|
||||
int main() {
|
||||
char* paste_url = nullptr;
|
||||
char* delete_token = nullptr;
|
||||
|
||||
int result = create_paste(
|
||||
"https://privatebin.net",
|
||||
"Hello, PrivateBin!",
|
||||
nullptr, // No password
|
||||
"1hour", // Expire in 1 hour
|
||||
"plaintext", // Plain text format
|
||||
0, // Don't burn after reading
|
||||
0, // No discussion
|
||||
&paste_url,
|
||||
&delete_token
|
||||
);
|
||||
|
||||
if (result == 0) {
|
||||
std::cout << "Paste created: " << paste_url << std::endl;
|
||||
std::cout << "Delete token: " << delete_token << std::endl;
|
||||
|
||||
// Free allocated memory
|
||||
free_string(paste_url);
|
||||
free_string(delete_token);
|
||||
} else {
|
||||
std::cout << "Failed to create paste: " << result << std::endl;
|
||||
}
|
||||
|
||||
return 0;
|
||||
char* paste_url = nullptr;
|
||||
char* delete_token = nullptr;
|
||||
|
||||
int result = create_paste(
|
||||
"https://privatebin.net",
|
||||
"Hello, World!",
|
||||
NULL, // no password
|
||||
"1day", // expiration
|
||||
"plaintext", // format
|
||||
0, // don't burn after reading
|
||||
0 // no discussion
|
||||
);
|
||||
|
||||
if (result == 0) {
|
||||
printf("Paste created: %s\n", paste_url);
|
||||
free_string(paste_url);
|
||||
free_string(delete_token);
|
||||
}
|
||||
```
|
||||
|
||||
## Error Codes
|
||||
#### Syntax Highlighting Example
|
||||
|
||||
- 0: Success
|
||||
- 1: Network error
|
||||
- 2: Encryption/decryption error
|
||||
- 3: Invalid input
|
||||
- 4: Server error
|
||||
- 5: JSON parsing error
|
||||
```c
|
||||
const char* code_content = R"(
|
||||
#include <iostream>
|
||||
|
||||
int main() {
|
||||
std::cout << "Hello, World!" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
)";
|
||||
|
||||
int result = create_paste(
|
||||
"https://privatebin.net",
|
||||
code_content,
|
||||
NULL, // no password
|
||||
"1week", // expiration
|
||||
"syntaxhighlighting", // syntax highlighting format
|
||||
0, // don't burn after reading
|
||||
0 // no discussion
|
||||
);
|
||||
```
|
||||
|
||||
#### Markdown Example
|
||||
|
||||
```c
|
||||
const char* markdown_content = R"(
|
||||
# Header
|
||||
|
||||
This is **bold** and *italic* text.
|
||||
|
||||
## Code Block
|
||||
```cpp
|
||||
int x = 42;
|
||||
std::cout << x << std::endl;
|
||||
```
|
||||
|
||||
> This is a blockquote.
|
||||
)";
|
||||
|
||||
int result = create_paste(
|
||||
"https://privatebin.net",
|
||||
markdown_content,
|
||||
NULL, // no password
|
||||
"1month", // expiration
|
||||
"markdown", // markdown format
|
||||
0, // don't burn after reading
|
||||
0 // no discussion
|
||||
);
|
||||
```
|
||||
|
||||
### File Upload
|
||||
|
||||
```c
|
||||
char* paste_url = nullptr;
|
||||
char* delete_token = nullptr;
|
||||
|
||||
int result = upload_file(
|
||||
"https://privatebin.net",
|
||||
"/path/to/file.txt",
|
||||
"mypassword", // optional password
|
||||
"1week", // expiration
|
||||
0, // don't burn after reading
|
||||
0 // no discussion
|
||||
);
|
||||
|
||||
if (result == 0) {
|
||||
printf("Successfully uploaded!\n");
|
||||
printf("URL: %s\n", paste_url);
|
||||
printf("Delete Token: %s\n", delete_token);
|
||||
|
||||
// Free memory
|
||||
free_string(paste_url);
|
||||
free_string(delete_token);
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
The library contains a comprehensive example program that demonstrates both text paste and file upload functionality:
|
||||
|
||||
### Running the Example
|
||||
|
||||
```bash
|
||||
# Run basic example (tests all text formats)
|
||||
./example
|
||||
|
||||
# Upload a file
|
||||
./example --upload https://privatebin.net test.txt
|
||||
|
||||
# Upload with password and expiration
|
||||
./example --upload https://privatebin.net test.txt mypassword 1week
|
||||
|
||||
# Upload with all options
|
||||
./example --upload https://privatebin.net test.txt mypassword 1day 1 0
|
||||
```
|
||||
|
||||
The basic example now demonstrates:
|
||||
- **Plaintext format** - Simple text pastes
|
||||
- **Syntax highlighting format** - Code with syntax highlighting
|
||||
- **Markdown format** - Rich text formatting
|
||||
- **Interactive format selection** - Shows available options
|
||||
|
||||
### Example Output
|
||||
|
||||
The example program demonstrates:
|
||||
- Creating text pastes
|
||||
- Retrieving paste content
|
||||
- Deleting pastes
|
||||
- File upload with various options
|
||||
- Error handling for all operations
|
||||
|
||||
## API Reference
|
||||
|
||||
### Functions
|
||||
|
||||
- `create_paste()` - Creates a text paste with optional formatting
|
||||
- `upload_file()` - Uploads a file
|
||||
- `get_paste()` - Retrieves a paste
|
||||
- `delete_paste()` - Deletes a paste
|
||||
- `free_string()` - Frees memory
|
||||
|
||||
### Format Support
|
||||
|
||||
The `create_paste()` function supports the following formats as defined in the [PrivateBin API v1.3](https://github.com/PrivateBin/PrivateBin/wiki/API):
|
||||
|
||||
- **`plaintext`** - Default format for simple text
|
||||
- **`syntaxhighlighting`** - Syntax highlighting for code snippets
|
||||
- **`markdown`** - Markdown formatting for rich text
|
||||
|
||||
### Error Codes
|
||||
|
||||
- `0` - Success
|
||||
- `1` - Network error
|
||||
- `2` - Cryptographic error
|
||||
- `3` - Invalid input (e.g., file not found or too large)
|
||||
- `4` - Server error
|
||||
- `5` - JSON parsing error
|
||||
|
||||
## Security
|
||||
|
||||
- **Client-Side Encryption**: All data is encrypted before upload
|
||||
- **AES-256-GCM**: Modern encryption with authentication
|
||||
- **PBKDF2**: Secure key derivation with 100,000 iterations
|
||||
- **Random Keys**: Each paste receives a unique key
|
||||
- **No Server Logs**: Server cannot read encrypted data
|
||||
- **File Compression**: Automatic zlib compression before encryption
|
||||
- **Binary Support**: All file types are treated as encrypted binary data
|
||||
|
||||
## Limitations
|
||||
|
||||
- **File Size**: Maximum file size is 100MB
|
||||
- **File Type**: All file types are treated as binary data
|
||||
- **Server Compatibility**: Works with PrivateBin v1.3+ servers
|
||||
- **Format**: Files are always treated as "plaintext" (even if they are binary data)
|
||||
|
||||
## Error Handling
|
||||
|
||||
The library returns detailed error codes and logs errors to the console. Common errors:
|
||||
|
||||
- **File not found**: Check the file path
|
||||
- **File too large**: Reduce file size or split it up
|
||||
- **Network error**: Check server URL and internet connection
|
||||
- **Server error**: The server might be temporarily unavailable
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
See [LICENSE](LICENSE) for details.
|
||||
|
||||
## Troubleshooting
|
||||
## Changelog
|
||||
|
||||
- vcpkg requires a baseline / "this vcpkg instance requires a manifest with a specified baseline"
|
||||
- Run in the repo root to add an initial builtin baseline to `vcpkg.json`:
|
||||
```powershell
|
||||
$env:VCPKG_ROOT = "$env:USERPROFILE\vcpkg"
|
||||
& "$env:VCPKG_ROOT\vcpkg.exe" x-update-baseline --add-initial-baseline
|
||||
```
|
||||
- Then configure/build again.
|
||||
### v0.1.1.5 (2025-08-28)
|
||||
- **NEW**: Enhanced text format support (plaintext, syntax highlighting, markdown)
|
||||
- **NEW**: Comprehensive format testing in examples and integration tests
|
||||
- **IMPROVED**: Better API documentation with format examples
|
||||
- **IMPROVED**: Enhanced test coverage for all supported formats
|
||||
|
||||
- Visual Studio instance not found / "could not find specified instance of Visual Studio"
|
||||
- Ensure VS 2022 Build Tools or Community with C++ tools and Windows 11 SDK are installed.
|
||||
- Use the Developer Command Prompt (VsDevCmd):
|
||||
```powershell
|
||||
cmd /c "call `"C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools\\Common7\\Tools\\VsDevCmd.bat`" -arch=x64 && build.bat"
|
||||
```
|
||||
- Or delete the build folder to clear stale CMake cache and reconfigure:
|
||||
```powershell
|
||||
Remove-Item -Recurse -Force build
|
||||
```
|
||||
### v0.1.1.4 (2025-08-28)
|
||||
- **NEW**: Automated release creation script (`scripts/create_release.ps1`)
|
||||
- **NEW**: Build scripts moved to `scripts/` directory for better organization
|
||||
- **IMPROVED**: Enhanced build documentation with platform-specific instructions
|
||||
- **IMPROVED**: Better project structure and organization
|
||||
|
||||
- `vswhere.exe` not found
|
||||
- Add the VS Installer directory to PATH for the current session:
|
||||
```powershell
|
||||
$env:PATH = 'C:\\Program Files\\Microsoft Visual Studio\\Installer;' + $env:PATH
|
||||
```
|
||||
- Or install/download `vswhere` from Microsoft and place it under the Installer folder.
|
||||
### v0.1.1.1 (2025-08-28)
|
||||
- **NEW**: Combined example program with both text and file upload functionality
|
||||
- **IMPROVED**: Unified command-line interface for examples
|
||||
- **IMPROVED**: Better error handling and user experience
|
||||
|
||||
- `cryptoppConfig.cmake` / `cryptopp-config.cmake` not found during CMake configure
|
||||
- Make sure CMake uses vcpkg's toolchain file and that the ports are installed for the active triplet:
|
||||
```powershell
|
||||
$env:VCPKG_ROOT = "$env:USERPROFILE\vcpkg"
|
||||
cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake"
|
||||
& "$env:VCPKG_ROOT\vcpkg.exe" install cryptopp nlohmann-json --triplet x64-windows
|
||||
```
|
||||
|
||||
- PowerShell line continuation issues / parser errors
|
||||
- Prefer single-line commands in PowerShell (avoid backticks if unsure). The README uses single-line examples for reliability.
|
||||
|
||||
## LLVM/clang-cl Coverage (Windows)
|
||||
|
||||
Requirements:
|
||||
- clang/clang-cl toolchain installed
|
||||
- LLVM tools on PATH (`llvm-profdata`, `llvm-cov`)
|
||||
|
||||
Configure with coverage:
|
||||
```powershell
|
||||
cmd /c "call ""C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\Common7\Tools\VsDevCmd.bat"" -arch=x64 && cmake -S . -B build-llvm -G "Ninja" -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang-cl -DENABLE_LLVM_COVERAGE=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=""%USERPROFILE%\vcpkg\scripts\buildsystems\vcpkg.cmake"" && cmake --build build-llvm --config Release"
|
||||
```
|
||||
|
||||
Run coverage target (executes tests, merges profiles, generates HTML report):
|
||||
```powershell
|
||||
cmd /c "call ""C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\Common7\Tools\VsDevCmd.bat"" -arch=x64 && cmake --build build-llvm --target coverage_llvm --config Release && start build-llvm\coverage\html\index.html"
|
||||
```
|
||||
|
||||
Notes:
|
||||
- If Ninja is not available, you can use the VS generator, but Ninja is recommended for clang.
|
||||
- You can override `LLVM_PROFDATA` and `LLVM_COV` cache variables to absolute tool paths if needed.
|
||||
### v0.1.1 (2025-08-28)
|
||||
- **NEW**: File upload functionality added
|
||||
- **NEW**: `upload_file()` function implemented
|
||||
- **NEW**: Comprehensive example program
|
||||
- **NEW**: Extended documentation for file upload
|
||||
- **IMPROVED**: Better error handling
|
||||
- **IMPROVED**: Cross-platform compatibility
|
||||
20
build.bat
20
build.bat
@ -1,20 +0,0 @@
|
||||
@echo off
|
||||
echo Building PrivateBin API C++ DLL...
|
||||
|
||||
REM Create build directory
|
||||
if not exist "build" mkdir build
|
||||
cd build
|
||||
|
||||
REM Generate build files with CMake and vcpkg manifest
|
||||
cmake .. -G "Visual Studio 17 2022" -DCMAKE_TOOLCHAIN_FILE=../vcpkg/scripts/buildsystems/vcpkg.cmake
|
||||
|
||||
REM Build the project
|
||||
cmake --build . --config Release
|
||||
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo Build completed successfully!
|
||||
) else (
|
||||
echo Build failed with error level %ERRORLEVEL%
|
||||
)
|
||||
|
||||
cd ..
|
||||
@ -1,101 +1,329 @@
|
||||
#include "privatebinapi.h"
|
||||
#include <iostream>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <cstdlib>
|
||||
|
||||
int main() {
|
||||
int main(int argc, char* argv[]) {
|
||||
std::cout << "PrivateBin API C++ DLL Example" << std::endl;
|
||||
std::cout << "===============================" << std::endl;
|
||||
|
||||
// Example of how to use the API
|
||||
char* paste_url = nullptr;
|
||||
char* delete_token = nullptr;
|
||||
|
||||
std::cout << "Creating paste on https://privatebin.medisoftware.org..." << std::endl;
|
||||
|
||||
// Testing against https://privatebin.medisoftware.org
|
||||
int result = create_paste(
|
||||
"https://privatebin.medisoftware.org", // Server URL
|
||||
"Hello, PrivateBin from C++ Library!", // Content
|
||||
nullptr, // No password
|
||||
"1hour", // Expire in 1 hour
|
||||
"plaintext", // Plain text format
|
||||
0, // Don't burn after reading
|
||||
0, // No discussion
|
||||
&paste_url, // Output: paste URL
|
||||
&delete_token // Output: delete token
|
||||
);
|
||||
|
||||
std::cout << "create_paste returned: " << result << std::endl;
|
||||
|
||||
if (result == 0) {
|
||||
std::cout << "Paste created successfully!" << std::endl;
|
||||
std::cout << "URL: " << paste_url << std::endl;
|
||||
std::cout << "Delete token: " << delete_token << std::endl;
|
||||
|
||||
// Parse paste_id and key from URL (format: "/?{pasteID}#{key}")
|
||||
std::string full_url = paste_url ? paste_url : "";
|
||||
std::string paste_id;
|
||||
std::string key;
|
||||
auto qpos = full_url.find('?');
|
||||
auto hpos = full_url.find('#');
|
||||
if (qpos != std::string::npos) {
|
||||
if (hpos != std::string::npos && hpos > qpos + 1) {
|
||||
paste_id = full_url.substr(qpos + 1, hpos - (qpos + 1));
|
||||
key = full_url.substr(hpos + 1);
|
||||
} else if (qpos + 1 < full_url.size()) {
|
||||
paste_id = full_url.substr(qpos + 1);
|
||||
}
|
||||
// Check if file upload mode is requested
|
||||
if (argc >= 3 && strcmp(argv[1], "--upload") == 0) {
|
||||
// File upload mode
|
||||
if (argc < 4) {
|
||||
std::cout << "Usage: " << argv[0] << " --upload <server_url> <file_path> [password] [expiration] [burn_after_reading] [open_discussion]" << std::endl;
|
||||
std::cout << "Example: " << argv[0] << " --upload https://privatebin.net test.txt mypassword 1day 0 0" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "Parameters:" << std::endl;
|
||||
std::cout << " server_url - URL of the PrivateBin server" << std::endl;
|
||||
std::cout << " file_path - Path to the file to upload" << std::endl;
|
||||
std::cout << " password - Optional: Password for the paste (default: none)" << std::endl;
|
||||
std::cout << " expiration - Optional: Expiration time (default: 1day)" << std::endl;
|
||||
std::cout << " burn_after_reading - Optional: Burn after reading (0=no, 1=yes, default: 0)" << std::endl;
|
||||
std::cout << " open_discussion - Optional: Allow discussion (0=no, 1=yes, default: 0)" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Try to fetch paste content back
|
||||
if (!paste_id.empty() && !key.empty()) {
|
||||
std::cout << "Fetching paste..." << std::endl;
|
||||
char* content = nullptr;
|
||||
int gr = get_paste("https://privatebin.medisoftware.org", paste_id.c_str(), key.c_str(), &content);
|
||||
std::cout << "get_paste returned: " << gr << std::endl;
|
||||
if (gr == 0 && content) {
|
||||
std::cout << "Content: " << content << std::endl;
|
||||
free_string(content);
|
||||
}
|
||||
}
|
||||
const char* server_url = argv[2];
|
||||
const char* file_path = argv[3];
|
||||
const char* password = (argc > 4) ? argv[4] : nullptr;
|
||||
const char* expiration = (argc > 5) ? argv[5] : "1day";
|
||||
int burn_after_reading = (argc > 6) ? std::atoi(argv[6]) : 0;
|
||||
int open_discussion = (argc > 7) ? std::atoi(argv[7]) : 0;
|
||||
|
||||
// Try to delete paste
|
||||
if (!paste_id.empty() && delete_token) {
|
||||
std::cout << "Deleting paste..." << std::endl;
|
||||
int dr = delete_paste("https://privatebin.medisoftware.org", paste_id.c_str(), delete_token);
|
||||
std::cout << "delete_paste returned: " << dr << std::endl;
|
||||
std::cout << "PrivateBin File Upload Example" << std::endl;
|
||||
std::cout << "===============================" << std::endl;
|
||||
std::cout << "Server: " << server_url << std::endl;
|
||||
std::cout << "File: " << file_path << std::endl;
|
||||
if (password) {
|
||||
std::cout << "Password: " << password << std::endl;
|
||||
}
|
||||
std::cout << "Expiration: " << expiration << std::endl;
|
||||
std::cout << "Burn after reading: " << (burn_after_reading ? "Yes" : "No") << std::endl;
|
||||
std::cout << "Allow discussion: " << (open_discussion ? "Yes" : "No") << std::endl;
|
||||
std::cout << std::endl;
|
||||
|
||||
// Clean up allocated memory
|
||||
free_string(paste_url);
|
||||
free_string(delete_token);
|
||||
} else {
|
||||
std::cout << "Failed to create paste. Error code: " << result << std::endl;
|
||||
char* paste_url = nullptr;
|
||||
char* delete_token = nullptr;
|
||||
|
||||
std::cout << "Uploading file..." << std::endl;
|
||||
|
||||
// Print error codes explanation
|
||||
switch(result) {
|
||||
case 1:
|
||||
std::cout << "Network error occurred." << std::endl;
|
||||
break;
|
||||
case 2:
|
||||
std::cout << "Encryption/decryption error occurred." << std::endl;
|
||||
break;
|
||||
case 3:
|
||||
std::cout << "Invalid input provided." << std::endl;
|
||||
break;
|
||||
case 4:
|
||||
std::cout << "Server error occurred." << std::endl;
|
||||
break;
|
||||
case 5:
|
||||
std::cout << "JSON parsing error occurred." << std::endl;
|
||||
break;
|
||||
default:
|
||||
std::cout << "Unknown error occurred." << std::endl;
|
||||
break;
|
||||
int result = upload_file(server_url, file_path, password, expiration,
|
||||
burn_after_reading, open_discussion,
|
||||
&paste_url, &delete_token);
|
||||
|
||||
if (result == 0) {
|
||||
std::cout << "Successfully uploaded!" << std::endl;
|
||||
std::cout << "Paste URL: " << paste_url << std::endl;
|
||||
std::cout << "Delete Token: " << delete_token << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "Note: Keep the delete token safe to delete the paste later." << std::endl;
|
||||
|
||||
// Free memory
|
||||
free_string(paste_url);
|
||||
free_string(delete_token);
|
||||
} else {
|
||||
std::cout << "Error uploading file!" << std::endl;
|
||||
std::cout << "Error code: " << result << std::endl;
|
||||
|
||||
switch (result) {
|
||||
case 1:
|
||||
std::cout << "Error: Network problem" << std::endl;
|
||||
break;
|
||||
case 2:
|
||||
std::cout << "Error: Cryptographic error" << std::endl;
|
||||
break;
|
||||
case 3:
|
||||
std::cout << "Error: Invalid input (e.g., file not found or too large)" << std::endl;
|
||||
break;
|
||||
case 4:
|
||||
std::cout << "Error: Server error" << std::endl;
|
||||
break;
|
||||
case 5:
|
||||
std::cout << "Error: JSON parsing error" << std::endl;
|
||||
break;
|
||||
default:
|
||||
std::cout << "Error: Unknown error" << std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (result == 0) ? 0 : 1;
|
||||
} else {
|
||||
// Interactive mode - show usage and run basic example
|
||||
std::cout << "Usage:" << std::endl;
|
||||
std::cout << " " << argv[0] << " - Run basic example" << std::endl;
|
||||
std::cout << " " << argv[0] << " --upload <args> - Upload a file" << std::endl;
|
||||
std::cout << std::endl;
|
||||
|
||||
// Example of how to use the API with different formats
|
||||
std::cout << "Testing different text formats..." << std::endl;
|
||||
|
||||
// Test 1: Plain text format
|
||||
std::cout << "\n1. Testing plaintext format..." << std::endl;
|
||||
char* paste_url = nullptr;
|
||||
char* delete_token = nullptr;
|
||||
|
||||
int result = create_paste(
|
||||
"https://privatebin.medisoftware.org", // Server URL
|
||||
"Hello, PrivateBin from C++ Library!\nThis is plain text.", // Content
|
||||
nullptr, // No password
|
||||
"1hour", // Expire in 1 hour
|
||||
"plaintext", // Plain text format
|
||||
0, // Don't burn after reading
|
||||
0, // No discussion
|
||||
&paste_url, // Output: paste URL
|
||||
&delete_token // Output: delete token
|
||||
);
|
||||
|
||||
if (result == 0) {
|
||||
std::cout << "✓ Plaintext paste created successfully!" << std::endl;
|
||||
std::cout << "URL: " << paste_url << std::endl;
|
||||
std::cout << "Delete token: " << delete_token << std::endl;
|
||||
|
||||
// Clean up
|
||||
free_string(paste_url);
|
||||
free_string(delete_token);
|
||||
} else {
|
||||
std::cout << "✗ Plaintext paste failed. Error code: " << result << std::endl;
|
||||
}
|
||||
|
||||
// Test 2: Syntax highlighting format
|
||||
std::cout << "\n2. Testing syntax highlighting format..." << std::endl;
|
||||
char* code_paste_url = nullptr;
|
||||
char* code_delete_token = nullptr;
|
||||
|
||||
const char* code_content = R"(
|
||||
#include <iostream>
|
||||
|
||||
int main() {
|
||||
std::cout << "Hello, World!" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
)";
|
||||
|
||||
result = create_paste(
|
||||
"https://privatebin.medisoftware.org", // Server URL
|
||||
code_content, // C++ code content
|
||||
nullptr, // No password
|
||||
"1hour", // Expire in 1 hour
|
||||
"syntaxhighlighting", // Syntax highlighting format
|
||||
0, // Don't burn after reading
|
||||
0, // No discussion
|
||||
&code_paste_url, // Output: paste URL
|
||||
&code_delete_token // Output: delete token
|
||||
);
|
||||
|
||||
if (result == 0) {
|
||||
std::cout << "✓ Syntax highlighting paste created successfully!" << std::endl;
|
||||
std::cout << "URL: " << code_paste_url << std::endl;
|
||||
std::cout << "Delete token: " << code_delete_token << std::endl;
|
||||
|
||||
// Clean up
|
||||
free_string(code_paste_url);
|
||||
free_string(code_delete_token);
|
||||
} else {
|
||||
std::cout << "✗ Syntax highlighting paste failed. Error code: " << result << std::endl;
|
||||
}
|
||||
|
||||
// Test 3: Markdown format
|
||||
std::cout << "\n3. Testing markdown format..." << std::endl;
|
||||
char* markdown_paste_url = nullptr;
|
||||
char* markdown_delete_token = nullptr;
|
||||
|
||||
const char* markdown_content = R"(
|
||||
# PrivateBin API C++ Library
|
||||
|
||||
## Features
|
||||
- **Text Paste Creation**: Create encrypted text pastes
|
||||
- **File Upload**: Upload files as encrypted pastes
|
||||
- **Multiple Formats**: Support for plaintext, syntax highlighting, and markdown
|
||||
|
||||
## Code Example
|
||||
```cpp
|
||||
int result = create_paste(server_url, content, password, expiration, format,
|
||||
burn_after_reading, open_discussion, &paste_url, &delete_token);
|
||||
```
|
||||
|
||||
> This is a blockquote demonstrating markdown support.
|
||||
)";
|
||||
|
||||
result = create_paste(
|
||||
"https://privatebin.medisoftware.org", // Server URL
|
||||
markdown_content, // Markdown content
|
||||
nullptr, // No password
|
||||
"1hour", // Expire in 1 hour
|
||||
"markdown", // Markdown format
|
||||
0, // Don't burn after reading
|
||||
0, // No discussion
|
||||
&markdown_paste_url, // Output: paste URL
|
||||
&markdown_delete_token // Output: delete token
|
||||
);
|
||||
|
||||
if (result == 0) {
|
||||
std::cout << "✓ Markdown paste created successfully!" << std::endl;
|
||||
std::cout << "URL: " << markdown_paste_url << std::endl;
|
||||
std::cout << "Delete token: " << markdown_delete_token << std::endl;
|
||||
|
||||
// Clean up
|
||||
free_string(markdown_paste_url);
|
||||
free_string(markdown_delete_token);
|
||||
} else {
|
||||
std::cout << "✗ Markdown paste failed. Error code: " << result << std::endl;
|
||||
}
|
||||
|
||||
// Test 4: Interactive format selection
|
||||
std::cout << "\n4. Interactive format selection..." << std::endl;
|
||||
char* interactive_paste_url = nullptr;
|
||||
char* interactive_delete_token = nullptr;
|
||||
|
||||
std::cout << "Available formats:" << std::endl;
|
||||
std::cout << " - plaintext (default)" << std::endl;
|
||||
std::cout << " - syntaxhighlighting" << std::endl;
|
||||
std::cout << " - markdown" << std::endl;
|
||||
|
||||
const char* interactive_content = "This paste demonstrates the interactive format selection feature.";
|
||||
|
||||
result = create_paste(
|
||||
"https://privatebin.medisoftware.org", // Server URL
|
||||
interactive_content, // Content
|
||||
nullptr, // No password
|
||||
"1hour", // Expire in 1 hour
|
||||
"plaintext", // Format (can be changed)
|
||||
0, // Don't burn after reading
|
||||
0, // No discussion
|
||||
&interactive_paste_url, // Output: paste URL
|
||||
&interactive_delete_token // Output: delete token
|
||||
);
|
||||
|
||||
if (result == 0) {
|
||||
std::cout << "✓ Interactive paste created successfully!" << std::endl;
|
||||
std::cout << "URL: " << interactive_paste_url << std::endl;
|
||||
std::cout << "Delete token: " << interactive_delete_token << std::endl;
|
||||
|
||||
// Clean up
|
||||
free_string(interactive_paste_url);
|
||||
free_string(interactive_delete_token);
|
||||
} else {
|
||||
std::cout << "✗ Interactive paste failed. Error code: " << result << std::endl;
|
||||
}
|
||||
|
||||
std::cout << "\nAll format tests completed!" << std::endl;
|
||||
return 0;
|
||||
|
||||
std::cout << "create_paste returned: " << result << std::endl;
|
||||
|
||||
if (result == 0) {
|
||||
std::cout << "Paste created successfully!" << std::endl;
|
||||
std::cout << "URL: " << paste_url << std::endl;
|
||||
std::cout << "Delete token: " << delete_token << std::endl;
|
||||
|
||||
// Parse paste_id and key from URL (format: "/?{pasteID}#{key}")
|
||||
std::string full_url = paste_url ? paste_url : "";
|
||||
std::string paste_id;
|
||||
std::string key;
|
||||
auto qpos = full_url.find('?');
|
||||
auto hpos = full_url.find('#');
|
||||
if (qpos != std::string::npos) {
|
||||
if (hpos != std::string::npos && hpos > qpos + 1) {
|
||||
paste_id = full_url.substr(qpos + 1, hpos - (qpos + 1));
|
||||
key = full_url.substr(hpos + 1);
|
||||
} else if (qpos + 1 < full_url.size()) {
|
||||
paste_id = full_url.substr(qpos + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to fetch paste content back
|
||||
if (!paste_id.empty() && !key.empty()) {
|
||||
std::cout << "Fetching paste..." << std::endl;
|
||||
char* content = nullptr;
|
||||
int gr = get_paste("https://privatebin.medisoftware.org", paste_id.c_str(), key.c_str(), &content);
|
||||
std::cout << "get_paste returned: " << gr << std::endl;
|
||||
if (gr == 0 && content) {
|
||||
std::cout << "Content: " << content << std::endl;
|
||||
free_string(content);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to delete paste
|
||||
if (!paste_id.empty() && delete_token) {
|
||||
std::cout << "Deleting paste..." << std::endl;
|
||||
int dr = delete_paste("https://privatebin.medisoftware.org", paste_id.c_str(), delete_token);
|
||||
std::cout << "delete_paste returned: " << dr << std::endl;
|
||||
}
|
||||
|
||||
// Clean up allocated memory
|
||||
free_string(paste_url);
|
||||
free_string(delete_token);
|
||||
} else {
|
||||
std::cout << "Failed to create paste. Error code: " << result << std::endl;
|
||||
|
||||
// Print error codes explanation
|
||||
switch(result) {
|
||||
case 1:
|
||||
std::cout << "Network error occurred." << std::endl;
|
||||
break;
|
||||
case 2:
|
||||
std::cout << "Encryption/decryption error occurred." << std::endl;
|
||||
break;
|
||||
case 3:
|
||||
std::cout << "Invalid input provided." << std::endl;
|
||||
break;
|
||||
case 4:
|
||||
std::cout << "Server error occurred." << std::endl;
|
||||
break;
|
||||
case 5:
|
||||
std::cout << "JSON parsing error occurred." << std::endl;
|
||||
break;
|
||||
default:
|
||||
std::cout << "Unknown error occurred." << std::endl;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << "Example completed." << std::endl;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::cout << "Example completed." << std::endl;
|
||||
|
||||
return result;
|
||||
}
|
||||
@ -35,6 +35,24 @@ PRIVATEBIN_API int create_paste(const char* server_url, const char* content,
|
||||
int open_discussion, char** paste_url,
|
||||
char** delete_token);
|
||||
|
||||
/**
|
||||
* Uploads a file to a PrivateBin server
|
||||
*
|
||||
* @param server_url The URL of the PrivateBin server
|
||||
* @param file_path The path to the file to upload
|
||||
* @param password Optional password for the paste (can be NULL)
|
||||
* @param expiration Expiration time ("5min", "10min", "1hour", "1day", "1week", "1month", "1year", "never")
|
||||
* @param burn_after_reading Set to 1 to enable burn after reading, 0 to disable
|
||||
* @param open_discussion Set to 1 to enable discussion, 0 to disable
|
||||
* @param paste_url Output parameter for the URL of the created paste
|
||||
* @param delete_token Output parameter for the deletion token
|
||||
* @return 0 on success, error code on failure
|
||||
*/
|
||||
PRIVATEBIN_API int upload_file(const char* server_url, const char* file_path,
|
||||
const char* password, const char* expiration,
|
||||
int burn_after_reading, int open_discussion,
|
||||
char** paste_url, char** delete_token);
|
||||
|
||||
/**
|
||||
* Retrieves a paste from a PrivateBin server
|
||||
*
|
||||
|
||||
62
scripts/build_windows.ps1
Normal file
62
scripts/build_windows.ps1
Normal file
@ -0,0 +1,62 @@
|
||||
# Requires: PowerShell, Visual Studio 2022 Build Tools (C++), vcpkg (will be bootstrapped), Git
|
||||
# Usage:
|
||||
# powershell -NoProfile -ExecutionPolicy Bypass -File .\build_windows.ps1
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
Write-Host "Building PrivateBin API C++ (Windows, x64, Release)" -ForegroundColor Cyan
|
||||
|
||||
# Resolve vcpkg root
|
||||
if (-not $env:VCPKG_ROOT) {
|
||||
$env:VCPKG_ROOT = Join-Path $env:USERPROFILE 'vcpkg'
|
||||
}
|
||||
|
||||
if (-not (Test-Path $env:VCPKG_ROOT)) {
|
||||
Write-Host "Cloning vcpkg into $env:VCPKG_ROOT ..."
|
||||
git clone https://github.com/microsoft/vcpkg $env:VCPKG_ROOT
|
||||
& "$env:VCPKG_ROOT\bootstrap-vcpkg.bat"
|
||||
}
|
||||
|
||||
# Choose generator (VS 2022)
|
||||
$generator = 'Visual Studio 17 2022'
|
||||
$arch = 'x64'
|
||||
|
||||
# Clean and configure
|
||||
if (Test-Path build) { Remove-Item -Recurse -Force build }
|
||||
cmake -S . -B build -G "$generator" -A $arch -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT\scripts\buildsystems\vcpkg.cmake"
|
||||
|
||||
# Build
|
||||
cmake --build build --config Release
|
||||
|
||||
Write-Host "Build completed." -ForegroundColor Green
|
||||
|
||||
# Package artifacts (zip)
|
||||
$dist = Join-Path (Get-Location) 'dist\windows'
|
||||
if (Test-Path $dist) { Remove-Item -Recurse -Force $dist }
|
||||
New-Item -ItemType Directory -Force -Path $dist | Out-Null
|
||||
|
||||
# Collect artifacts
|
||||
$binDir = Join-Path (Get-Location) 'build\Release'
|
||||
$exampleDir = Join-Path (Get-Location) 'build\example\Release'
|
||||
|
||||
$dll = Join-Path $binDir 'privatebinapi.dll'
|
||||
$lib = Join-Path $binDir 'privatebinapi.lib'
|
||||
$pdb = Join-Path $binDir 'privatebinapi.pdb'
|
||||
$exe = Join-Path $exampleDir 'example.exe'
|
||||
|
||||
Copy-Item -Path $dll -Destination $dist -ErrorAction SilentlyContinue
|
||||
Copy-Item -Path $lib -Destination $dist -ErrorAction SilentlyContinue
|
||||
Copy-Item -Path $pdb -Destination $dist -ErrorAction SilentlyContinue
|
||||
Copy-Item -Path $exe -Destination $dist -ErrorAction SilentlyContinue
|
||||
Copy-Item -Path (Join-Path (Get-Location) 'include') -Destination $dist -Recurse
|
||||
Copy-Item -Path (Join-Path (Get-Location) 'README.md') -Destination $dist
|
||||
Copy-Item -Path (Join-Path (Get-Location) 'LICENSE') -Destination $dist -ErrorAction SilentlyContinue
|
||||
|
||||
$zipPath = Join-Path (Get-Location) 'dist\lib-privatebin-v0.1.1.3-windows-x64.zip'
|
||||
if (Test-Path $zipPath) { Remove-Item -Force $zipPath }
|
||||
Compress-Archive -Path (Join-Path $dist '*') -DestinationPath $zipPath
|
||||
|
||||
Write-Host "Windows artifact packaged:" -ForegroundColor Cyan
|
||||
Write-Host " $zipPath"
|
||||
|
||||
33
scripts/collect_binaries.ps1
Normal file
33
scripts/collect_binaries.ps1
Normal file
@ -0,0 +1,33 @@
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$OutDir
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
|
||||
|
||||
$items = @(
|
||||
@{ Path = 'build/Release/privatebinapi.dll'; Optional = $false }
|
||||
@{ Path = 'build/Release/privatebinapi.lib'; Optional = $true }
|
||||
@{ Path = 'build/Release/privatebinapi.pdb'; Optional = $true }
|
||||
@{ Path = 'build/example/Release/example.exe'; Optional = $true }
|
||||
)
|
||||
|
||||
foreach ($it in $items) {
|
||||
$p = Resolve-Path -LiteralPath $it.Path -ErrorAction SilentlyContinue
|
||||
if ($null -ne $p) {
|
||||
Copy-Item -LiteralPath $p.Path -Destination $OutDir -Force
|
||||
Write-Host ("Collected: " + [IO.Path]::GetFileName($p.Path))
|
||||
} elseif (-not $it.Optional) {
|
||||
throw "Required artifact not found: $($it.Path)"
|
||||
} else {
|
||||
Write-Host ("Skip missing optional: " + $it.Path)
|
||||
}
|
||||
}
|
||||
|
||||
Get-ChildItem -LiteralPath $OutDir -File | Format-Table Name,Length -AutoSize
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
148
scripts/create_release.ps1
Normal file
148
scripts/create_release.ps1
Normal file
@ -0,0 +1,148 @@
|
||||
param(
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Token
|
||||
)
|
||||
|
||||
# Token aus Umgebungsvariable laden falls nicht als Parameter übergeben
|
||||
if (-not $Token) {
|
||||
$Token = $env:GITEA_TOKEN
|
||||
if (-not $Token) {
|
||||
Write-Host "Fehler: Kein Token angegeben und GITEA_TOKEN Umgebungsvariable nicht gesetzt!" -ForegroundColor Red
|
||||
Write-Host "Verwendung: .\create_release.ps1 -Token 'your_token' oder setze GITEA_TOKEN Umgebungsvariable" -ForegroundColor Yellow
|
||||
Write-Host "Das Script führt automatisch einen Build durch und lädt alle Artefakte hoch." -ForegroundColor Cyan
|
||||
exit 1
|
||||
}
|
||||
Write-Host "Token aus Umgebungsvariable GITEA_TOKEN geladen" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host "=== lib-privatebin Release Creator ===" -ForegroundColor Cyan
|
||||
|
||||
# Aktuelle Version ermitteln
|
||||
$lastTag = git describe --tags --abbrev=0 2>$null
|
||||
if (-not $lastTag) {
|
||||
$lastTag = "v0.1.0"
|
||||
}
|
||||
|
||||
Write-Host "Letzter Tag: $lastTag" -ForegroundColor Green
|
||||
|
||||
# Version parsen
|
||||
if ($lastTag -match "^v(\d+)\.(\d+)\.(\d+)(.*)$") {
|
||||
$major = [int]$matches[1]
|
||||
$minor = [int]$matches[2]
|
||||
$patch = [int]$matches[3]
|
||||
$suffix = $matches[4]
|
||||
|
||||
$newPatch = $patch + 1
|
||||
$newVersion = "v$major.$minor.$newPatch$suffix"
|
||||
|
||||
Write-Host "Neue Version: $newVersion" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "Fehler: Ungültiges Versionsformat: $lastTag" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Build durchführen
|
||||
Write-Host "Führe Build durch..." -ForegroundColor Yellow
|
||||
if (Test-Path "scripts\build_thinkpad.bat") {
|
||||
Write-Host "Verwende scripts\build_thinkpad.bat..." -ForegroundColor Yellow
|
||||
cmd /c scripts\build_thinkpad.bat
|
||||
} else {
|
||||
Write-Host "Verwende scripts\build_windows.ps1..." -ForegroundColor Yellow
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\build_windows.ps1
|
||||
}
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Build fehlgeschlagen!" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
Write-Host "Build erfolgreich abgeschlossen!" -ForegroundColor Green
|
||||
|
||||
# Änderungen committen
|
||||
$status = git status --porcelain
|
||||
if ($status) {
|
||||
git add -A
|
||||
git commit -m "Release $newVersion prepare for release"
|
||||
git push origin HEAD
|
||||
}
|
||||
|
||||
# Tag erstellen und pushen
|
||||
git tag -a $newVersion -m "Release $newVersion"
|
||||
git push origin $newVersion
|
||||
|
||||
# Release erstellen
|
||||
$releaseBody = "## What is New in $newVersion`n`n- AUTOMATED: Release created by script`n- VERSION: Bumped from $lastTag to $newVersion`n- BUILD: Automatic build with build_thinkpad.bat`n`n## Build Artifacts`n`nBuild-Artefakte werden automatisch hochgeladen und hier angezeigt.`n`nTypische Artefakte:`n- privatebinapi.dll - Windows Dynamic Link Library`n- privatebinapi.lib - Windows Import Library`n- example.exe - Combined example program`n- privatebinapi.h - C++ header file"
|
||||
|
||||
$releaseData = @{
|
||||
tag_name = $newVersion
|
||||
name = "$newVersion - Automated Release"
|
||||
body = $releaseBody
|
||||
draft = $false
|
||||
prerelease = $false
|
||||
} | ConvertTo-Json -Depth 10
|
||||
|
||||
$headers = @{
|
||||
"Authorization" = "token $Token"
|
||||
"Content-Type" = "application/json"
|
||||
}
|
||||
|
||||
$releaseUri = "https://gitea.medisoftware.org/api/v1/repos/Markus/lib-privatebin/releases"
|
||||
$release = Invoke-RestMethod -Uri $releaseUri -Method Post -Headers $headers -Body $releaseData
|
||||
|
||||
Write-Host "Release erstellt: $($release.id)" -ForegroundColor Green
|
||||
Write-Host "URL: $($release.html_url)" -ForegroundColor Green
|
||||
|
||||
# Build-Artefakte zum Release hinzufügen
|
||||
Write-Host "Füge Build-Artefakte hinzu..." -ForegroundColor Yellow
|
||||
|
||||
# Artefakte aus dem dist-Ordner finden
|
||||
$distPath = "dist"
|
||||
if (Test-Path $distPath) {
|
||||
$artifacts = Get-ChildItem -Path $distPath -Recurse -File | Where-Object {
|
||||
$_.Extension -match "\.(dll|lib|exe|h|zip)$"
|
||||
}
|
||||
|
||||
if ($artifacts) {
|
||||
Write-Host "Gefundene Artefakte:" -ForegroundColor Green
|
||||
foreach ($artifact in $artifacts) {
|
||||
Write-Host " - $($artifact.Name)" -ForegroundColor White
|
||||
}
|
||||
|
||||
# Artefakte hochladen
|
||||
foreach ($artifact in $artifacts) {
|
||||
Write-Host "Lade hoch: $($artifact.Name)..." -ForegroundColor Yellow
|
||||
|
||||
$uploadUri = "https://gitea.medisoftware.org/api/v1/repos/Markus/lib-privatebin/releases/$($release.id)/assets"
|
||||
|
||||
$boundary = [System.Guid]::NewGuid().ToString()
|
||||
$LF = "`r`n"
|
||||
$bodyLines = @(
|
||||
"--$boundary",
|
||||
"Content-Disposition: form-data; name=`"attachment`"; filename=`"$($artifact.Name)`"",
|
||||
"Content-Type: application/octet-stream",
|
||||
"",
|
||||
[System.IO.File]::ReadAllBytes($artifact.FullName),
|
||||
"--$boundary--"
|
||||
)
|
||||
|
||||
$body = $bodyLines -join $LF
|
||||
|
||||
$uploadHeaders = @{
|
||||
"Authorization" = "token $Token"
|
||||
"Content-Type" = "multipart/form-data; boundary=$boundary"
|
||||
}
|
||||
|
||||
try {
|
||||
$uploadResponse = Invoke-RestMethod -Uri $uploadUri -Method Post -Headers $uploadHeaders -Body $body
|
||||
Write-Host " ✓ $($artifact.Name) erfolgreich hochgeladen" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host " ✗ Fehler beim Hochladen von $($artifact.Name): $($_.Exception.Message)" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Host "Keine Build-Artefakte im dist-Ordner gefunden!" -ForegroundColor Yellow
|
||||
}
|
||||
} else {
|
||||
Write-Host "dist-Ordner nicht gefunden!" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host "Release-Erstellung abgeschlossen!" -ForegroundColor Green
|
||||
|
||||
38
scripts/gen_checksums.ps1
Normal file
38
scripts/gen_checksums.ps1
Normal file
@ -0,0 +1,38 @@
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$ZipPath,
|
||||
[Parameter(Mandatory=$true)][string]$BinDir
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ZipPath)) {
|
||||
throw "ZIP not found: $ZipPath"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $BinDir)) {
|
||||
throw "BinDir not found: $BinDir"
|
||||
}
|
||||
|
||||
function Write-ChecksumFile {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$Path
|
||||
)
|
||||
$hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $Path).Hash.ToLower()
|
||||
$outfile = "$Path.sha256"
|
||||
$line = "$hash $([System.IO.Path]::GetFileName($Path))"
|
||||
Set-Content -Path $outfile -NoNewline -Encoding ASCII -Value $line
|
||||
Write-Host "Wrote $outfile"
|
||||
}
|
||||
|
||||
# ZIP checksum
|
||||
Write-ChecksumFile -Path $ZipPath
|
||||
|
||||
# Binaries checksums
|
||||
Get-ChildItem -LiteralPath $BinDir -File | ForEach-Object {
|
||||
Write-ChecksumFile -Path $_.FullName
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
46
scripts/gitea_upload.ps1
Normal file
46
scripts/gitea_upload.ps1
Normal file
@ -0,0 +1,46 @@
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$Server,
|
||||
[Parameter(Mandatory=$true)][string]$Owner,
|
||||
[Parameter(Mandatory=$true)][string]$Repo,
|
||||
[Parameter(Mandatory=$true)][string]$Token,
|
||||
[Parameter(Mandatory=$true)][string]$ZipPath
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ZipPath)) {
|
||||
throw "ZIP not found: $ZipPath"
|
||||
}
|
||||
|
||||
$headers = @{ Authorization = "token $Token" }
|
||||
|
||||
$latestUrl = "$Server/api/v1/repos/$Owner/$Repo/releases/latest"
|
||||
$latest = Invoke-RestMethod -Headers $headers -Method GET -Uri $latestUrl
|
||||
if (-not $latest -or -not $latest.id) {
|
||||
throw "Failed to get latest release from $latestUrl"
|
||||
}
|
||||
|
||||
$rid = [string]$latest.id
|
||||
$name = [System.IO.Path]::GetFileName($ZipPath)
|
||||
$escapedName = [System.Uri]::EscapeDataString($name)
|
||||
$uploadUrl = "$Server/api/v1/repos/$Owner/$Repo/releases/$rid/assets?name=$escapedName"
|
||||
|
||||
Write-Host "Latest release id: $rid"
|
||||
Write-Host "Uploading $name to $uploadUrl"
|
||||
|
||||
# Use Invoke-WebRequest multipart form upload
|
||||
$form = @{ attachment = Get-Item -LiteralPath $ZipPath }
|
||||
$resp = Invoke-WebRequest -Headers $headers -Method Post -Uri $uploadUrl -Form $form
|
||||
|
||||
if ($resp.StatusCode -ge 200 -and $resp.StatusCode -lt 300) {
|
||||
Write-Host "Upload successful (HTTP $($resp.StatusCode))"
|
||||
} else {
|
||||
Write-Host "Upload failed (HTTP $($resp.StatusCode))" -ForegroundColor Red
|
||||
if ($resp.Content) { Write-Host $resp.Content }
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
48
scripts/gitea_upload_all.ps1
Normal file
48
scripts/gitea_upload_all.ps1
Normal file
@ -0,0 +1,48 @@
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$Server,
|
||||
[Parameter(Mandatory=$true)][string]$Owner,
|
||||
[Parameter(Mandatory=$true)][string]$Repo,
|
||||
[Parameter(Mandatory=$true)][string]$Token,
|
||||
[Parameter(Mandatory=$true)][string]$Directory
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Directory)) {
|
||||
throw "Directory not found: $Directory"
|
||||
}
|
||||
|
||||
$headers = @{ Authorization = "token $Token" }
|
||||
|
||||
$latestUrl = "$Server/api/v1/repos/$Owner/$Repo/releases/latest"
|
||||
$latest = Invoke-RestMethod -Headers $headers -Method GET -Uri $latestUrl
|
||||
if (-not $latest -or -not $latest.id) {
|
||||
throw "Failed to get latest release from $latestUrl"
|
||||
}
|
||||
$rid = [string]$latest.id
|
||||
Write-Host "Latest release id: $rid"
|
||||
|
||||
$files = Get-ChildItem -LiteralPath $Directory -Recurse -File
|
||||
if (-not $files) {
|
||||
Write-Host "No files to upload in $Directory"
|
||||
exit 0
|
||||
}
|
||||
|
||||
foreach ($f in $files) {
|
||||
$name = $f.Name
|
||||
$escapedName = [System.Uri]::EscapeDataString($name)
|
||||
$uploadUrl = "$Server/api/v1/repos/$Owner/$Repo/releases/$rid/assets?name=$escapedName"
|
||||
Write-Host "Uploading $name ..."
|
||||
try {
|
||||
$resp = Invoke-WebRequest -Headers $headers -Method Post -Uri $uploadUrl -Form @{ attachment = $f }
|
||||
Write-Host " OK ($($resp.StatusCode))"
|
||||
}
|
||||
catch {
|
||||
Write-Host " FAIL: $name -> $($_.Exception.Message)" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
89
scripts/manage_release.ps1
Normal file
89
scripts/manage_release.ps1
Normal file
@ -0,0 +1,89 @@
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$Server,
|
||||
[Parameter(Mandatory=$true)][string]$Owner,
|
||||
[Parameter(Mandatory=$true)][string]$Repo,
|
||||
[Parameter(Mandatory=$true)][string]$Token
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$headers = @{ Authorization = "token $Token" }
|
||||
|
||||
# Get latest release
|
||||
$latest = Invoke-RestMethod -Headers $headers -Method GET -Uri "$Server/api/v1/repos/$Owner/$Repo/releases/latest"
|
||||
if (-not $latest -or -not $latest.id) {
|
||||
throw "Failed to get latest release"
|
||||
}
|
||||
$rid = [string]$latest.id
|
||||
Write-Host "Operating on release id: $rid"
|
||||
|
||||
# Load full release including attachments/assets list
|
||||
$release = Invoke-RestMethod -Headers $headers -Method GET -Uri "$Server/api/v1/repos/$Owner/$Repo/releases/$rid"
|
||||
$assets = $release.assets
|
||||
if (-not $assets) { $assets = $release.attachments }
|
||||
|
||||
if ($assets) {
|
||||
$groups = $assets | Group-Object -Property name
|
||||
foreach ($g in $groups) {
|
||||
$sorted = $g.Group | Sort-Object -Property id -Descending
|
||||
if ($sorted.Count -gt 1) {
|
||||
$keep = $sorted[0]
|
||||
foreach ($extra in $sorted[1..($sorted.Count-1)]) {
|
||||
if ($null -ne $extra -and $extra.id -ne $keep.id) {
|
||||
$delUrl = "$Server/api/v1/repos/$Owner/$Repo/releases/assets/$($extra.id)"
|
||||
try {
|
||||
Invoke-RestMethod -Headers $headers -Method DELETE -Uri $delUrl | Out-Null
|
||||
Write-Host ("Deleted duplicate asset: " + $extra.name + " (id=" + $extra.id + ")")
|
||||
} catch {
|
||||
Write-Host ("Failed to delete asset id=" + $extra.id + ": " + $_.Exception.Message) -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Host 'No assets found on release.'
|
||||
}
|
||||
|
||||
# Compose release notes with known checksums if they exist locally
|
||||
$zipShaPath = (Resolve-Path 'dist\lib-privatebin-v0.1.1.3-windows-x64.zip.sha256' -ErrorAction SilentlyContinue)
|
||||
$dllShaPath = (Resolve-Path 'dist\windows_bin\privatebinapi.dll.sha256' -ErrorAction SilentlyContinue)
|
||||
$libShaPath = (Resolve-Path 'dist\windows_bin\privatebinapi.lib.sha256' -ErrorAction SilentlyContinue)
|
||||
$exeShaPath = (Resolve-Path 'dist\windows_bin\example.exe.sha256' -ErrorAction SilentlyContinue)
|
||||
|
||||
$lines = @('# Downloadable artifacts', '')
|
||||
$lines += ("- [lib-privatebin-v0.1.1.3-windows-x64.zip]($Server/$Owner/$Repo/releases/download/$tag/lib-privatebin-v0.1.1.3-windows-x64.zip)")
|
||||
$lines += ("- [privatebinapi.dll]($Server/$Owner/$Repo/releases/download/$tag/privatebinapi.dll)")
|
||||
$lines += ("- [privatebinapi.lib]($Server/$Owner/$Repo/releases/download/$tag/privatebinapi.lib)")
|
||||
$lines += ("- [example.exe]($Server/$Owner/$Repo/releases/download/$tag/example.exe)")
|
||||
$lines += ''
|
||||
$lines += '# SHA256 checksums'
|
||||
if ($zipShaPath) { $lines += ("- [lib-privatebin-v0.1.1.3-windows-x64.zip.sha256]($Server/$Owner/$Repo/releases/download/$tag/lib-privatebin-v0.1.1.3-windows-x64.zip.sha256)") }
|
||||
if ($dllShaPath) { $lines += ("- [privatebinapi.dll.sha256]($Server/$Owner/$Repo/releases/download/$tag/privatebinapi.dll.sha256)") }
|
||||
if ($libShaPath) { $lines += ("- [privatebinapi.lib.sha256]($Server/$Owner/$Repo/releases/download/$tag/privatebinapi.lib.sha256)") }
|
||||
if ($exeShaPath) { $lines += ("- [example.exe.sha256]($Server/$Owner/$Repo/releases/download/$tag/example.exe.sha256)") }
|
||||
|
||||
$lines += ''
|
||||
$lines += '# Changes'
|
||||
try {
|
||||
$changelog = Get-Content -Raw -LiteralPath 'CHANGELOG.md'
|
||||
$sections = $changelog -split "\r?\n## "
|
||||
$section = $sections | Where-Object { $_ -like 'v0.1.1.3*' } | Select-Object -First 1
|
||||
if ($section) {
|
||||
$secLines = $section -split "\r?\n"
|
||||
if ($secLines[0] -like 'v0.1.1.3*') { $secLines[0] = 'v0.1.1.3' }
|
||||
$lines += $secLines
|
||||
} else {
|
||||
$lines += 'See CHANGELOG.md'
|
||||
}
|
||||
} catch {
|
||||
$lines += 'See CHANGELOG.md'
|
||||
}
|
||||
|
||||
$bodyText = ($lines -join "`n")
|
||||
$payload = @{ body = $bodyText } | ConvertTo-Json -Depth 3
|
||||
|
||||
Invoke-RestMethod -Headers (@{ Authorization = ("token " + $Token); 'Content-Type' = 'application/json' }) -Method PATCH -Uri "$Server/api/v1/repos/$Owner/$Repo/releases/$rid" -Body $payload | Out-Null
|
||||
Write-Host 'Release notes updated.'
|
||||
|
||||
|
||||
79
scripts/prune_release_assets.ps1
Normal file
79
scripts/prune_release_assets.ps1
Normal file
@ -0,0 +1,79 @@
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][string]$Server,
|
||||
[Parameter(Mandatory=$true)][string]$Owner,
|
||||
[Parameter(Mandatory=$true)][string]$Repo,
|
||||
[Parameter(Mandatory=$true)][string]$Token
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$headers = @{ Authorization = "token $Token" }
|
||||
|
||||
$latest = Invoke-RestMethod -Headers $headers -Method GET -Uri "$Server/api/v1/repos/$Owner/$Repo/releases/latest"
|
||||
if (-not $latest -or -not $latest.id) { throw 'Failed to get latest release' }
|
||||
$rid = [string]$latest.id
|
||||
$tag = $latest.tag_name
|
||||
Write-Host "Pruning assets on release id=$rid (tag=$tag)"
|
||||
|
||||
$release = Invoke-RestMethod -Headers $headers -Method GET -Uri "$Server/api/v1/repos/$Owner/$Repo/releases/$rid"
|
||||
$assets = $release.assets
|
||||
if (-not $assets) { $assets = $release.attachments }
|
||||
|
||||
# Allowed asset names
|
||||
$allowed = @(
|
||||
'lib-privatebin-v0.1.1.3-windows-x64.zip',
|
||||
'lib-privatebin-v0.1.1.3-windows-x64.zip.sha256',
|
||||
'privatebinapi.dll','privatebinapi.dll.sha256',
|
||||
'privatebinapi.lib','privatebinapi.lib.sha256',
|
||||
'example.exe','example.exe.sha256'
|
||||
)
|
||||
|
||||
if ($assets) {
|
||||
foreach ($a in $assets) {
|
||||
if ($allowed -notcontains $a.name) {
|
||||
$delUrl = "$Server/api/v1/repos/$Owner/$Repo/releases/assets/$($a.id)"
|
||||
try {
|
||||
Invoke-RestMethod -Headers $headers -Method DELETE -Uri $delUrl | Out-Null
|
||||
Write-Host ("Deleted: " + $a.name)
|
||||
} catch {
|
||||
Write-Host ("Skip delete (" + $a.name + "): " + $_.Exception.Message) -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Host 'No assets found.'
|
||||
}
|
||||
|
||||
# Re-fetch assets after prune
|
||||
$release = Invoke-RestMethod -Headers $headers -Method GET -Uri "$Server/api/v1/repos/$Owner/$Repo/releases/$rid"
|
||||
$assets = $release.assets; if (-not $assets) { $assets = $release.attachments }
|
||||
|
||||
# Build markdown with links
|
||||
$lines = @('# Downloadable artifacts', '')
|
||||
foreach ($name in $allowed) {
|
||||
$a = $assets | Where-Object { $_.name -eq $name } | Select-Object -First 1
|
||||
if ($a) {
|
||||
# Prefer deterministic download URL pattern
|
||||
$url = "$Server/$Owner/$Repo/releases/download/$tag/$name"
|
||||
$lines += ("- [$name]($url)")
|
||||
}
|
||||
}
|
||||
|
||||
$lines += ''
|
||||
$lines += '# SHA256 checksums'
|
||||
foreach ($name in $allowed | Where-Object { $_.EndsWith('.sha256') }) {
|
||||
$a = $assets | Where-Object { $_.name -eq $name } | Select-Object -First 1
|
||||
if ($a) {
|
||||
$url = "$Server/$Owner/$Repo/releases/download/$tag/$name"
|
||||
$lines += ("- [$name]($url)")
|
||||
}
|
||||
}
|
||||
|
||||
$bodyText = ($lines -join "`n")
|
||||
$payload = @{ body = $bodyText } | ConvertTo-Json -Depth 3
|
||||
Invoke-RestMethod -Headers (@{ Authorization = ("token " + $Token); 'Content-Type' = 'application/json' }) -Method PATCH -Uri "$Server/api/v1/repos/$Owner/$Repo/releases/$rid" -Body $payload | Out-Null
|
||||
Write-Host 'Release notes updated with links.'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -8,6 +8,8 @@
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#define PRIVATEBIN_API_VERSION "1.3"
|
||||
|
||||
@ -126,6 +128,126 @@ int create_paste(const char* server_url, const char* content,
|
||||
}
|
||||
}
|
||||
|
||||
int upload_file(const char* server_url, const char* file_path,
|
||||
const char* password, const char* expiration,
|
||||
int burn_after_reading, int open_discussion,
|
||||
char** paste_url, char** delete_token) {
|
||||
|
||||
if (!server_url || !file_path) {
|
||||
return ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
try {
|
||||
// Read the file content
|
||||
std::ifstream file(file_path, std::ios::binary);
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "[privatebinapi] Failed to open file: " << file_path << std::endl;
|
||||
return ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
// Get file size
|
||||
file.seekg(0, std::ios::end);
|
||||
std::streamsize file_size = file.tellg();
|
||||
file.seekg(0, std::ios::beg);
|
||||
|
||||
// Check if file is too large (PrivateBin typically has limits)
|
||||
if (file_size > 100 * 1024 * 1024) { // 100MB limit
|
||||
std::cerr << "[privatebinapi] File too large: " << file_size << " bytes" << std::endl;
|
||||
return ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
// Read file content into vector
|
||||
std::vector<unsigned char> file_content(file_size);
|
||||
if (!file.read(reinterpret_cast<char*>(file_content.data()), file_size)) {
|
||||
std::cerr << "[privatebinapi] Failed to read file content" << std::endl;
|
||||
return ERROR_INVALID_INPUT;
|
||||
}
|
||||
file.close();
|
||||
|
||||
// Generate a random 32-byte paste key
|
||||
std::vector<unsigned char> paste_key = Crypto::generate_key(32);
|
||||
|
||||
// If password provided, append it to the paste key
|
||||
std::string paste_passphrase(reinterpret_cast<char*>(paste_key.data()), paste_key.size());
|
||||
if (password) {
|
||||
paste_passphrase += password;
|
||||
}
|
||||
|
||||
// Compress the file content
|
||||
std::cerr << "[privatebinapi] compress file..." << std::endl;
|
||||
std::vector<unsigned char> compressed_data = Crypto::compress(file_content);
|
||||
|
||||
// Generate random salt and IV
|
||||
std::vector<unsigned char> salt = Crypto::generate_key(8);
|
||||
std::vector<unsigned char> iv = Crypto::generate_key(16);
|
||||
|
||||
// Derive key using PBKDF2
|
||||
std::cerr << "[privatebinapi] pbkdf2..." << std::endl;
|
||||
std::vector<unsigned char> derived_key = Crypto::pbkdf2_hmac_sha256(
|
||||
paste_passphrase, salt, 100000, 32);
|
||||
|
||||
// Encrypt the data
|
||||
std::cerr << "[privatebinapi] encrypt file..." << std::endl;
|
||||
std::vector<unsigned char> auth_tag;
|
||||
std::vector<unsigned char> cipher_text = Crypto::encrypt(
|
||||
compressed_data, derived_key, iv, auth_tag);
|
||||
|
||||
// Create the JSON structure for file upload
|
||||
json paste_json = JsonParser::create_paste_json(
|
||||
cipher_text, auth_tag, iv, salt,
|
||||
expiration ? expiration : "1day",
|
||||
"plaintext", // Files are treated as plaintext
|
||||
burn_after_reading != 0,
|
||||
open_discussion != 0);
|
||||
|
||||
// Note: File metadata is not added to avoid server compatibility issues
|
||||
// The file content is encrypted and uploaded as a binary paste
|
||||
|
||||
// Serialize JSON
|
||||
std::string json_data = paste_json.dump();
|
||||
|
||||
// Send POST request
|
||||
HttpClient client;
|
||||
std::string response;
|
||||
if (!client.post(server_url, json_data, response)) {
|
||||
return ERROR_NETWORK;
|
||||
}
|
||||
|
||||
// Parse response
|
||||
int status;
|
||||
std::string message, paste_id, url, del_token;
|
||||
if (!JsonParser::parse_response(response, status, message, paste_id, url, del_token)) {
|
||||
std::cerr << "[privatebinapi] raw response (unparsed): " << response << std::endl;
|
||||
return ERROR_JSON_PARSE;
|
||||
}
|
||||
if (status != 0) {
|
||||
std::cerr << "[privatebinapi] server status=" << status << ", message= " << message << std::endl;
|
||||
}
|
||||
|
||||
if (status != 0) {
|
||||
return ERROR_SERVER;
|
||||
}
|
||||
|
||||
// Encode the paste key with Base58
|
||||
std::string encoded_key = Base58::encode(paste_key);
|
||||
|
||||
// Construct the full URL
|
||||
std::string full_url = url + "#" + encoded_key;
|
||||
|
||||
// Copy results to output parameters
|
||||
copy_string_to_output(full_url, paste_url);
|
||||
copy_string_to_output(del_token, delete_token);
|
||||
|
||||
return ERROR_SUCCESS;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[privatebinapi] file upload error: " << e.what() << std::endl;
|
||||
return ERROR_CRYPTO;
|
||||
} catch (...) {
|
||||
std::cerr << "[privatebinapi] unknown file upload error" << std::endl;
|
||||
return ERROR_CRYPTO;
|
||||
}
|
||||
}
|
||||
|
||||
int get_paste(const char* server_url, const char* paste_id,
|
||||
const char* key, char** content) {
|
||||
|
||||
|
||||
12
test_file.txt
Normal file
12
test_file.txt
Normal file
@ -0,0 +1,12 @@
|
||||
This is a test file for the PrivateBin API File Upload functionality.
|
||||
|
||||
The file contains:
|
||||
- German umlauts: äöüß
|
||||
- Special characters: !@#$%^&*()_+-=[]{}|;':",./<>?
|
||||
- Numbers: 0123456789
|
||||
- Multi-line text
|
||||
|
||||
This file is used to test whether the upload_file function works correctly.
|
||||
|
||||
PrivateBin API v1.3
|
||||
File Upload Feature successfully implemented!
|
||||
@ -26,22 +26,27 @@ int main() {
|
||||
std::cout << "[test] PRIVATEBIN_IT not set; skipping integration test." << std::endl;
|
||||
return 0; // treat as success when integration testing is disabled
|
||||
}
|
||||
|
||||
const char* server = "https://privatebin.medisoftware.org/";
|
||||
const char* content = "Integration test from lib-privatebin";
|
||||
const char* password = nullptr; // no password
|
||||
const char* expiration = "5min"; // short-lived
|
||||
const char* format = "plaintext";
|
||||
const int burn_after_reading = 0;
|
||||
const int open_discussion = 0;
|
||||
|
||||
std::cout << "[test] Testing different text formats..." << std::endl;
|
||||
|
||||
// Test 1: Plaintext format
|
||||
std::cout << "[test] 1. Testing plaintext format..." << std::endl;
|
||||
const char* plaintext_content = "Integration test from lib-privatebin - plaintext";
|
||||
const char* plaintext_format = "plaintext";
|
||||
|
||||
char* paste_url = nullptr;
|
||||
char* delete_token = nullptr;
|
||||
|
||||
std::cout << "[test] create_paste..." << std::endl;
|
||||
int rc = create_paste(server, content, password, expiration, format,
|
||||
int rc = create_paste(server, plaintext_content, password, expiration, plaintext_format,
|
||||
burn_after_reading, open_discussion, &paste_url, &delete_token);
|
||||
if (rc != 0 || paste_url == nullptr || delete_token == nullptr) {
|
||||
std::cerr << "[test][ERROR] create_paste failed, rc=" << rc << std::endl;
|
||||
std::cerr << "[test][ERROR] plaintext create_paste failed, rc=" << rc << std::endl;
|
||||
if (paste_url) free_string(paste_url);
|
||||
if (delete_token) free_string(delete_token);
|
||||
return 1;
|
||||
@ -68,7 +73,7 @@ int main() {
|
||||
return 1;
|
||||
}
|
||||
std::string fetched_str = fetched;
|
||||
assert(fetched_str == content && "fetched content mismatch");
|
||||
assert(fetched_str == plaintext_content && "fetched plaintext content mismatch");
|
||||
|
||||
std::cout << "[test] delete_paste..." << std::endl;
|
||||
rc = delete_paste(server, paste_id.c_str(), delete_token);
|
||||
@ -80,11 +85,142 @@ int main() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// cleanup
|
||||
// cleanup plaintext test
|
||||
free_string(paste_url);
|
||||
free_string(delete_token);
|
||||
free_string(fetched);
|
||||
|
||||
// Test 2: Syntax highlighting format
|
||||
std::cout << "[test] 2. Testing syntax highlighting format..." << std::endl;
|
||||
const char* code_content = R"(
|
||||
#include <iostream>
|
||||
|
||||
std::cout << "[test] all API functions passed." << std::endl;
|
||||
int main() {
|
||||
std::cout << "Hello, World!" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
)";
|
||||
const char* code_format = "syntaxhighlighting";
|
||||
|
||||
char* code_paste_url = nullptr;
|
||||
char* code_delete_token = nullptr;
|
||||
|
||||
rc = create_paste(server, code_content, password, expiration, code_format,
|
||||
burn_after_reading, open_discussion, &code_paste_url, &code_delete_token);
|
||||
if (rc != 0 || code_paste_url == nullptr || code_delete_token == nullptr) {
|
||||
std::cerr << "[test][ERROR] syntax highlighting create_paste failed, rc=" << rc << std::endl;
|
||||
if (code_paste_url) free_string(code_paste_url);
|
||||
if (code_delete_token) free_string(code_delete_token);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Parse code paste URL
|
||||
std::string code_full_url = code_paste_url;
|
||||
std::string code_paste_id;
|
||||
std::string code_key;
|
||||
const bool code_ok_parse = extract_paste_id_and_key(code_full_url, code_paste_id, code_key);
|
||||
if (!code_ok_parse) {
|
||||
std::cerr << "[test][ERROR] failed to parse code paste id/key from URL: " << code_full_url << std::endl;
|
||||
free_string(code_paste_url);
|
||||
free_string(code_delete_token);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Fetch and verify code paste
|
||||
char* code_fetched = nullptr;
|
||||
rc = get_paste(server, code_paste_id.c_str(), code_key.c_str(), &code_fetched);
|
||||
if (rc != 0 || code_fetched == nullptr) {
|
||||
std::cerr << "[test][ERROR] code get_paste failed, rc=" << rc << std::endl;
|
||||
free_string(code_paste_url);
|
||||
free_string(code_delete_token);
|
||||
return 1;
|
||||
}
|
||||
std::string code_fetched_str = code_fetched;
|
||||
assert(code_fetched_str == code_content && "fetched code content mismatch");
|
||||
|
||||
// Delete code paste
|
||||
rc = delete_paste(server, code_paste_id.c_str(), code_delete_token);
|
||||
if (rc != 0) {
|
||||
std::cerr << "[test][ERROR] code delete_paste failed, rc=" << rc << std::endl;
|
||||
free_string(code_paste_url);
|
||||
free_string(code_delete_token);
|
||||
free_string(code_fetched);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// cleanup code test
|
||||
free_string(code_paste_url);
|
||||
free_string(code_delete_token);
|
||||
free_string(code_fetched);
|
||||
|
||||
// Test 3: Markdown format
|
||||
std::cout << "[test] 3. Testing markdown format..." << std::endl;
|
||||
const char* markdown_content = R"(
|
||||
# Test Header
|
||||
|
||||
This is a **bold** and *italic* text.
|
||||
|
||||
## Code Block
|
||||
```cpp
|
||||
int x = 42;
|
||||
std::cout << x << std::endl;
|
||||
```
|
||||
|
||||
> This is a blockquote.
|
||||
)";
|
||||
const char* markdown_format = "markdown";
|
||||
|
||||
char* markdown_paste_url = nullptr;
|
||||
char* markdown_delete_token = nullptr;
|
||||
|
||||
rc = create_paste(server, markdown_content, password, expiration, markdown_format,
|
||||
burn_after_reading, open_discussion, &markdown_paste_url, &markdown_delete_token);
|
||||
if (rc != 0 || markdown_paste_url == nullptr || markdown_delete_token == nullptr) {
|
||||
std::cerr << "[test][ERROR] markdown create_paste failed, rc=" << rc << std::endl;
|
||||
if (markdown_paste_url) free_string(markdown_paste_url);
|
||||
if (markdown_delete_token) free_string(markdown_delete_token);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Parse markdown paste URL
|
||||
std::string markdown_full_url = markdown_paste_url;
|
||||
std::string markdown_paste_id;
|
||||
std::string markdown_key;
|
||||
const bool markdown_ok_parse = extract_paste_id_and_key(markdown_full_url, markdown_paste_id, markdown_key);
|
||||
if (!markdown_ok_parse) {
|
||||
std::cerr << "[test][ERROR] failed to parse markdown paste id/key from URL: " << markdown_full_url << std::endl;
|
||||
free_string(markdown_paste_url);
|
||||
free_string(markdown_delete_token);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Fetch and verify markdown paste
|
||||
char* markdown_fetched = nullptr;
|
||||
rc = get_paste(server, markdown_paste_id.c_str(), markdown_key.c_str(), &markdown_fetched);
|
||||
if (rc != 0 || markdown_fetched == nullptr) {
|
||||
std::cerr << "[test][ERROR] markdown get_paste failed, rc=" << rc << std::endl;
|
||||
free_string(markdown_paste_url);
|
||||
free_string(markdown_delete_token);
|
||||
return 1;
|
||||
}
|
||||
std::string markdown_fetched_str = markdown_fetched;
|
||||
assert(markdown_fetched_str == markdown_content && "fetched markdown content mismatch");
|
||||
|
||||
// Delete markdown paste
|
||||
rc = delete_paste(server, markdown_paste_id.c_str(), markdown_delete_token);
|
||||
if (rc != 0) {
|
||||
std::cerr << "[test][ERROR] markdown delete_paste failed, rc=" << rc << std::endl;
|
||||
free_string(markdown_paste_url);
|
||||
free_string(markdown_delete_token);
|
||||
free_string(markdown_fetched);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// cleanup markdown test
|
||||
free_string(markdown_paste_url);
|
||||
free_string(markdown_delete_token);
|
||||
free_string(markdown_fetched);
|
||||
|
||||
std::cout << "[test] All format tests passed successfully!" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user