Skip to content
Open
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
130 changes: 127 additions & 3 deletions lib/src/data_source.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:file_picker_writable/file_picker_writable.dart';
import 'package:path/path.dart' as p;
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_charset_detector/flutter_charset_detector.dart';
Expand Down Expand Up @@ -178,16 +180,101 @@ class NativeDataSource extends DataSource {
FutureOr<NativeDataSource> resolveRelative(String relativePath) async {
if (parentDirIdentifier == null) {
throw OrgroError(
'Cant resolve path relative to this document',
"Can't resolve path relative to this document",
localizedMessage: (context) =>
AppLocalizations.of(context)!.errorCannotResolveRelativePath,
);
}
// TODO(aaron): See if we can resolve to a non-existent file for writing
try {
final resolved = await FilePickerWritable().resolveRelativePath(
directoryIdentifier: parentDirIdentifier!,
relativePath: relativePath,
);
if (resolved is! FileInfo) {
throw OrgroError(
'$relativePath resolved to a non-file: $resolved',
localizedMessage: (context) => AppLocalizations.of(
context,
)!.errorPathResolvedToNonFile(relativePath, resolved.uri),
);
}
return NativeDataSource(
resolved.fileName ?? Uri.parse(resolved.uri).pathSegments.last,
resolved.identifier,
resolved.uri,
persistable: resolved.persistable,
);
} on Exception {
// Normal resolution failed; try symlink-aware resolution.
// This handles git's portable symlink format where symlinks are stored
// as small text files containing the target path.
return _resolveWithSymlinks(relativePath);
}
}

/// Resolves a relative path while handling git-style symlinks.
///
/// Git stores symlinks as small text files containing the target path.
/// This method detects such files and follows them during path resolution.
Future<NativeDataSource> _resolveWithSymlinks(String relativePath) async {
final components = p.posix.split(relativePath);
String currentDirId = parentDirIdentifier!;

// Resolve each directory component, following symlinks as needed
for (int i = 0; i < components.length - 1; i++) {
final component = components[i];
if (component.isEmpty || component == '.') continue;

final resolved = await FilePickerWritable().resolveRelativePath(
directoryIdentifier: currentDirId,
relativePath: component,
);

if (resolved is DirectoryInfo) {
// Normal directory, continue traversal
currentDirId = resolved.identifier;
} else if (resolved is FileInfo) {
// Might be a git symlink file; try to read and follow it
final symlinkTarget = await _readGitSymlink(resolved);
if (symlinkTarget != null) {
// It's a symlink! Resolve the target path relative to current dir
final targetResolved = await FilePickerWritable().resolveRelativePath(
directoryIdentifier: currentDirId,
relativePath: symlinkTarget,
);
if (targetResolved is DirectoryInfo) {
currentDirId = targetResolved.identifier;
} else {
throw OrgroError(
'Symlink target "$symlinkTarget" is not a directory',
localizedMessage: (context) =>
AppLocalizations.of(context)!.errorPathResolvedToNonFile(
symlinkTarget,
targetResolved.uri,
),
);
}
} else {
throw OrgroError(
'Path component "$component" is a file, not a directory',
localizedMessage: (context) =>
AppLocalizations.of(context)!.errorPathResolvedToNonFile(
component,
resolved.uri,
),
);
}
}
}

// Resolve the final component (the actual file)
final lastComponent = components.last;
final resolved = await FilePickerWritable().resolveRelativePath(
directoryIdentifier: parentDirIdentifier!,
relativePath: relativePath,
directoryIdentifier: currentDirId,
relativePath: lastComponent,
);

if (resolved is! FileInfo) {
throw OrgroError(
'$relativePath resolved to a non-file: $resolved',
Expand All @@ -196,6 +283,7 @@ class NativeDataSource extends DataSource {
)!.errorPathResolvedToNonFile(relativePath, resolved.uri),
);
}

return NativeDataSource(
resolved.fileName ?? Uri.parse(resolved.uri).pathSegments.last,
resolved.identifier,
Expand All @@ -204,6 +292,42 @@ class NativeDataSource extends DataSource {
);
}

/// Reads a file and returns its contents if it looks like a git symlink.
///
/// Git symlinks are small text files (typically < 1KB) containing a relative
/// path with no newlines or special characters.
Future<String?> _readGitSymlink(FileInfo fileInfo) async {
try {
final bytes = await FilePickerWritable().readFile(
identifier: fileInfo.identifier,
reader: (_, file) => file.readAsBytes(),
);

// Git symlinks are very small (just a path)
if (bytes.length > 1024) return null;

// Check for binary content (null bytes or high bytes in first portion)
for (int i = 0; i < bytes.length && i < 100; i++) {
if (bytes[i] == 0 || bytes[i] > 127) return null;
}

final contents = utf8.decode(bytes).trim();

// Validate it looks like a relative path
if (contents.isEmpty ||
contents.contains('\n') ||
contents.contains('\r') ||
contents.length > 256 ||
contents.startsWith('/')) {
return null;
}

return contents;
} on Exception {
return null;
}
}

Future<FileInfo> write(String content) => FilePickerWritable().writeFile(
identifier: identifier,
writer: (file) => file.writeAsString(content),
Expand Down
2 changes: 1 addition & 1 deletion pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,7 @@ packages:
source: hosted
version: "3.2.1"
path:
dependency: transitive
dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dependencies:
org_flutter: ^10.3.1
# org_flutter:
# path: ../org_flutter
path: ^1.8.0
path_provider: ^2.0.9
share_plus: ^12.0.0
shared_preferences: ^2.0.0
Expand Down