Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ The release run heads these entries with the version and opens a fresh

## Unreleased

- **Breaking**: `DecodePreference` becomes `DecodeOptions`, gains a `csv` field
and is all `open` takes besides the file and logger. `CsvFile::from_file` and
`::with_options` go β€” use `DecodeOptions::as(type)` / `::as_csv(options)`.

- **Breaking**: naming a type reports the engine's own refusal, so
`open(f, DecodeOptions::as(FileType::rich_text_format))` on non-rtf bytes
throws `NoRtfFile`, not `UnknownFileType`. Detection alone is unchanged.

- `TextEncoding` and its five lookups now reach the java, python, objc and wasm
bindings, and a text file reports `encoding()` beside the `charset()` it
keeps. wasm gets it as `Odr.enums.TextEncoding`. Nothing is removed.
Expand Down
27 changes: 21 additions & 6 deletions apple/include/OdrCoreObjC/ODRFile.h
Original file line number Diff line number Diff line change
Expand Up @@ -216,13 +216,28 @@ NS_SWIFT_NAME(FileMeta)
+ (instancetype)new NS_UNAVAILABLE;
@end

/// How to decode a file, when the caller knows better than detection does.
NS_SWIFT_NAME(DecodePreference)
@interface ODRDecodePreference : NSObject
/// How to read a csv file. An unset field is detected from the file's opening
/// bytes; a set one is taken as given.
NS_SWIFT_NAME(CsvOptions)
@interface ODRCsvOptions : NSObject
/// `nil` to detect.
@property(nonatomic, strong, nullable) NSNumber *encoding;
/// A one-character string, `nil` to detect.
@property(nonatomic, copy, nullable) NSString *separator;
/// A one-character string, `nil` to detect.
@property(nonatomic, copy, nullable) NSString *quote;
@end

/// How to decode a file. Every field is optional; the default detects
/// everything.
NS_SWIFT_NAME(DecodeOptions)
@interface ODRDecodeOptions : NSObject
/// Decode as this type, whatever detection says. `nil` to let it decide.
@property(nonatomic, strong, nullable) NSNumber *asFileType;
/// Types to prefer, most preferred first.
@property(nonatomic, copy) NSArray<NSNumber *> *fileTypePriority;
/// Format-specific overrides for a file decoded as csv.
@property(nonatomic, strong) ODRCsvOptions *csv;
@end

/// A file, decoded or not β€” `odr::File`.
Expand Down Expand Up @@ -261,11 +276,11 @@ NS_SWIFT_NAME(DecodedFile)
as:(ODRFileType)type
error:(NSError **)error
NS_SWIFT_NAME(decode(path:as:));
/// Decodes the file at `path` following `preference`.
/// Decodes the file at `path` per `options`.
+ (nullable instancetype)decodePath:(NSString *)path
preference:(ODRDecodePreference *)preference
options:(ODRDecodeOptions *)options
error:(NSError **)error
NS_SWIFT_NAME(decode(path:preference:));
NS_SWIFT_NAME(decode(path:options:));
/// Decodes an already-open file.
+ (nullable instancetype)decodeFile:(ODRFile *)file
error:(NSError **)error
Expand Down
41 changes: 30 additions & 11 deletions apple/src/ODRFile.mm
Original file line number Diff line number Diff line change
Expand Up @@ -196,13 +196,19 @@ + (instancetype)metaWithHandle:(const odr::FileMeta &)handle {

@end

#pragma mark - ODRDecodePreference
#pragma mark - ODRCsvOptions

@implementation ODRDecodePreference
@implementation ODRCsvOptions
@end

#pragma mark - ODRDecodeOptions

@implementation ODRDecodeOptions

- (instancetype)init {
if ((self = [super init]) != nil) {
_fileTypePriority = @[];
_csv = [[ODRCsvOptions alloc] init];
}
return self;
}
Expand Down Expand Up @@ -323,20 +329,32 @@ + (nullable instancetype)decodePath:(NSString *)path
return guarded(error, [&]() -> ODRDecodedFile * {
return [ODRDecodedFile
decodedFileWithHandle:odr::open(to_string(path),
static_cast<odr::FileType>(type))];
odr::DecodeOptions::as(
static_cast<odr::FileType>(type)))];
});
}

+ (nullable instancetype)decodePath:(NSString *)path
preference:(ODRDecodePreference *)preference
options:(ODRDecodeOptions *)options
error:(NSError **)error {
return guarded(error, [&]() -> ODRDecodedFile * {
odr::DecodePreference native;
if (preference.asFileType != nil) {
odr::DecodeOptions native;
if (options.asFileType != nil) {
native.as_file_type =
static_cast<odr::FileType>(preference.asFileType.integerValue);
static_cast<odr::FileType>(options.asFileType.integerValue);
}
native.file_type_priority = to_file_types(options.fileTypePriority);
if (options.csv.encoding != nil) {
native.csv.encoding =
static_cast<odr::TextEncoding>(options.csv.encoding.integerValue);
}
// one character, and an empty string means unset rather than a NUL
if (options.csv.separator.length > 0) {
native.csv.separator = [options.csv.separator characterAtIndex:0];
}
if (options.csv.quote.length > 0) {
native.csv.quote = [options.csv.quote characterAtIndex:0];
}
native.file_type_priority = to_file_types(preference.fileTypePriority);
return [ODRDecodedFile
decodedFileWithHandle:odr::open(to_string(path), native)];
});
Expand All @@ -353,7 +371,7 @@ + (nullable instancetype)decodePath:(NSString *)path
error:(NSError **)error {
return guarded(error, [&]() -> ODRDecodedFile * {
return [ODRDecodedFile
decodedFileWithHandle:odr::open(to_string(path), logger.handle)];
decodedFileWithHandle:odr::open(to_string(path), {}, logger.handle)];
});
}

Expand All @@ -364,7 +382,8 @@ + (nullable instancetype)decodePath:(NSString *)path
return guarded(error, [&]() -> ODRDecodedFile * {
return [ODRDecodedFile
decodedFileWithHandle:odr::open(to_string(path),
static_cast<odr::FileType>(type),
odr::DecodeOptions::as(
static_cast<odr::FileType>(type)),
logger.handle)];
});
}
Expand All @@ -374,7 +393,7 @@ + (nullable instancetype)decodeFile:(ODRFile *)file
error:(NSError **)error {
return guarded(error, [&]() -> ODRDecodedFile * {
return [ODRDecodedFile
decodedFileWithHandle:odr::open(file.handle, logger.handle)];
decodedFileWithHandle:odr::open(file.handle, {}, logger.handle)];
});
}

Expand Down
8 changes: 4 additions & 4 deletions cli/src/server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ int main(const int argc, char **argv) {
password = argv[2];
}

DecodePreference decode_preference;
decode_preference.as_file_type = FileType::zip;

DecodedFile decoded_file = open(input, decode_preference, logger);
// the server offers the container's own entries beside the render, so a
// package is opened as the zip it is
DecodedFile decoded_file =
open(input, DecodeOptions::as(FileType::zip), logger);

if (decoded_file.password_encrypted()) {
if (!password) {
Expand Down
3 changes: 2 additions & 1 deletion jni/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ add_jar(odr_java
"java/app/opendocument/core/Bookmark.java"
"java/app/opendocument/core/BreakType.java"
"java/app/opendocument/core/Color.java"
"java/app/opendocument/core/DecodePreference.java"
"java/app/opendocument/core/CsvOptions.java"
"java/app/opendocument/core/DecodeOptions.java"
"java/app/opendocument/core/DecodedFile.java"
"java/app/opendocument/core/DirectionalMeasure.java"
"java/app/opendocument/core/DirectionalString.java"
Expand Down
29 changes: 29 additions & 0 deletions jni/java/app/opendocument/core/CsvOptions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package app.opendocument.core;

/**
* How to read a csv file. Mirrors {@code odr::CsvOptions}. An unset field is detected from the
* file's opening bytes; a set one is taken as given.
*/
public final class CsvOptions {
/** {@code null} to detect. */
public TextEncoding encoding;

/** {@code null} to detect. */
public Character separator;

/** {@code null} to detect. */
public Character quote;

// Flattened for the native layer: -1 for an unset field.
int encodingNative() {
return encoding == null ? -1 : encoding.toNative();
}

int separatorNative() {
return separator == null ? -1 : separator;
}

int quoteNative() {
return quote == null ? -1 : quote;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@
import java.util.ArrayList;
import java.util.List;

/** Preference for decoding files. Mirrors {@code odr::DecodePreference}. */
public final class DecodePreference {
/**
* How to decode a file. Mirrors {@code odr::DecodeOptions}. Every field is optional; the default
* detects everything.
*/
public final class DecodeOptions {
/** Decode as this file type; {@code null} to detect. */
public FileType asFileType;

/** Preferred types, most preferred first, among those detected. */
public List<FileType> fileTypePriority = new ArrayList<>();

/** Format-specific overrides for a file decoded as csv. */
public CsvOptions csv = new CsvOptions();

// Flattened for the native layer.
int asFileTypeNative() {
return asFileType == null ? -1 : asFileType.toNative();
Expand Down
22 changes: 15 additions & 7 deletions jni/java/app/opendocument/core/Odr.java
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,16 @@ public static DecodedFile open(String path, FileType as) {
return new DecodedFile(openAsNative(path, as.toNative()));
}

/** Opens and decodes a file with a decode preference. */
public static DecodedFile open(String path, DecodePreference preference) {
/** Opens and decodes a file, per {@code options}. */
public static DecodedFile open(String path, DecodeOptions options) {
return new DecodedFile(
openWithPreferenceNative(
openWithOptionsNative(
path,
preference.asFileTypeNative(),
preference.fileTypePriorityNative()));
options.asFileTypeNative(),
options.fileTypePriorityNative(),
options.csv.encodingNative(),
options.csv.separatorNative(),
options.csv.quoteNative()));
}

private static native int[] allFileTypesNative();
Expand Down Expand Up @@ -171,8 +174,13 @@ public static DecodedFile open(String path, DecodePreference preference) {

private static native long openAsNative(String path, int as);

private static native long openWithPreferenceNative(
String path, int asFileType, int[] fileTypePriority);
private static native long openWithOptionsNative(
String path,
int asFileType,
int[] fileTypePriority,
int csvEncoding,
int csvSeparator,
int csvQuote);

private Odr() {}
}
28 changes: 20 additions & 8 deletions jni/src/jni_core.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -232,37 +232,49 @@ Java_app_opendocument_core_Odr_openWithLoggerNative(JNIEnv *env, jclass,
jlong logger) {
return guarded(env, [&] {
return make_handle(
odr::open(to_string(env, path), *from_handle<odr::Logger>(logger)));
odr::open(to_string(env, path), {}, *from_handle<odr::Logger>(logger)));
});
}

extern "C" JNIEXPORT jlong JNICALL Java_app_opendocument_core_Odr_openAsNative(
JNIEnv *env, jclass, jstring path, jint as) {
return guarded(env, [&] {
return make_handle(
odr::open(to_string(env, path), static_cast<odr::FileType>(as)));
odr::open(to_string(env, path),
odr::DecodeOptions::as(static_cast<odr::FileType>(as))));
});
}

extern "C" JNIEXPORT jlong JNICALL
Java_app_opendocument_core_Odr_openWithPreferenceNative(
Java_app_opendocument_core_Odr_openWithOptionsNative(
JNIEnv *env, jclass, jstring path, jint as_file_type,
jintArray file_type_priority) {
jintArray file_type_priority, jint csv_encoding, jint csv_separator,
jint csv_quote) {
return guarded(env, [&] {
odr::DecodePreference preference;
odr::DecodeOptions options;
if (as_file_type >= 0) {
preference.as_file_type = static_cast<odr::FileType>(as_file_type);
options.as_file_type = static_cast<odr::FileType>(as_file_type);
}
if (jint *codes = env->GetIntArrayElements(file_type_priority, nullptr);
codes != nullptr) {
const jsize length = env->GetArrayLength(file_type_priority);
for (jsize i = 0; i < length; ++i) {
preference.file_type_priority.push_back(
options.file_type_priority.push_back(
static_cast<odr::FileType>(codes[i]));
}
env->ReleaseIntArrayElements(file_type_priority, codes, JNI_ABORT);
}
return make_handle(odr::open(to_string(env, path), preference));
// -1 is how java spells an unset field across the boundary
if (csv_encoding >= 0) {
options.csv.encoding = static_cast<odr::TextEncoding>(csv_encoding);
}
if (csv_separator >= 0) {
options.csv.separator = static_cast<char>(csv_separator);
}
if (csv_quote >= 0) {
options.csv.quote = static_cast<char>(csv_quote);
}
return make_handle(odr::open(to_string(env, path), options));
});
}

Expand Down
15 changes: 15 additions & 0 deletions jni/tests/app/opendocument/core/FileTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
Expand Down Expand Up @@ -118,6 +119,20 @@ void openMissingFileThrows() {
() -> Odr.open(tempDir.resolve("missing.odt").toString()));
}

@Test
void openCarriesCsvOptions() throws IOException {
Path path = Files.writeString(tempDir.resolve("semicolons.csv"), "a;b\n1;2\n");

// detection would find the semicolon; a pipe it would not, so the caller says
DecodeOptions options = new DecodeOptions();
options.asFileType = FileType.COMMA_SEPARATED_VALUES;
options.csv.separator = '|';

try (DecodedFile file = Odr.open(path.toString(), options)) {
assertEquals(FileType.COMMA_SEPARATED_VALUES, file.fileType());
}
}

@Test
void listFileTypes() throws IOException {
Path odt = TestFiles.odtFile(tempDir);
Expand Down
Loading
Loading