@@ -18,6 +18,8 @@ mod error;
1818mod with_serde;
1919
2020pub use error:: TryFromBytesError ;
21+ pub use error:: InsertError ;
22+ pub use error:: RemoveError ;
2123pub use error:: TryFromStrError ;
2224
2325#[ derive( Clone , Copy ) ]
@@ -150,19 +152,190 @@ impl<const CAPACITY: usize> PascalString<CAPACITY> {
150152 ///
151153 /// This mirrors `String::push_str`’s “cannot fail” ergonomics; use `try_push_str` if you want a recoverable error.
152154 #[ inline]
155+ #[ deprecated( note = "PascalString is fixed-capacity; prefer `try_push_str`, `push_str_truncated`, or `push_str_expect_capacity`." ) ]
153156 pub fn push_str ( & mut self , string : & str ) {
154- self . try_push_str ( string)
155- . expect ( "PascalString capacity exceeded" ) ;
157+ self . push_str_expect_capacity ( string) ;
156158 }
157159
158160 /// Appends a character, panicking if the capacity would be exceeded.
159161 ///
160162 /// This mirrors `String::push`’s “cannot fail” ergonomics; use `try_push` if you want a recoverable error.
161163 #[ inline]
164+ #[ deprecated( note = "PascalString is fixed-capacity; prefer `try_push`, `push_str_truncated`, or `push_expect_capacity`." ) ]
162165 pub fn push ( & mut self , ch : char ) {
166+ self . push_expect_capacity ( ch) ;
167+ }
168+
169+ /// Appends a string slice, panicking if the capacity would be exceeded.
170+ #[ inline]
171+ pub fn push_str_expect_capacity ( & mut self , string : & str ) {
172+ self . try_push_str ( string)
173+ . expect ( "PascalString capacity exceeded" ) ;
174+ }
175+
176+ /// Appends a character, panicking if the capacity would be exceeded.
177+ #[ inline]
178+ pub fn push_expect_capacity ( & mut self , ch : char ) {
163179 self . try_push ( ch) . expect ( "PascalString capacity exceeded" ) ;
164180 }
165181
182+ /// Inserts a string slice at the given byte index.
183+ ///
184+ /// This is a true `try_` API: it **never panics**. All failure modes are returned as `InsertError`.
185+ #[ inline]
186+ pub fn try_insert_str ( & mut self , idx : usize , string : & str ) -> Result < ( ) , InsertError > {
187+ let len = self . len ( ) ;
188+ if idx > len {
189+ return Err ( InsertError :: OutOfBounds { idx, len } ) ;
190+ }
191+ if !self . is_char_boundary ( idx) {
192+ return Err ( InsertError :: NotCharBoundary { idx } ) ;
193+ }
194+
195+ let insert_len = string. len ( ) ;
196+ let new_len = len + insert_len;
197+ if new_len > CAPACITY {
198+ return Err ( InsertError :: TooLong ) ;
199+ }
200+
201+ // Shift tail to make room.
202+ self . data . copy_within ( idx..len, idx + insert_len) ;
203+ // Copy inserted bytes.
204+ self . data [ idx..idx + insert_len] . copy_from_slice ( string. as_bytes ( ) ) ;
205+ self . len = new_len as u8 ;
206+ Ok ( ( ) )
207+ }
208+
209+ /// Inserts a string slice at the given byte index, truncating the inserted string to available capacity.
210+ ///
211+ /// Returns the remainder that did not fit.
212+ ///
213+ /// This is a true `try_` API: it **never panics**. Index/boundary errors are returned as `InsertError`.
214+ #[ inline]
215+ pub fn try_insert_str_truncated < ' s > (
216+ & mut self ,
217+ idx : usize ,
218+ string : & ' s str ,
219+ ) -> Result < & ' s str , InsertError > {
220+ let len = self . len ( ) ;
221+ if idx > len {
222+ return Err ( InsertError :: OutOfBounds { idx, len } ) ;
223+ }
224+ if !self . is_char_boundary ( idx) {
225+ return Err ( InsertError :: NotCharBoundary { idx } ) ;
226+ }
227+
228+ let available = CAPACITY . saturating_sub ( len) ;
229+ if available >= string. len ( ) {
230+ self . try_insert_str ( idx, string) ?;
231+ return Ok ( "" ) ;
232+ }
233+
234+ let mut prefix_len = 0 ;
235+ for c in string. chars ( ) {
236+ let l = c. len_utf8 ( ) ;
237+ if prefix_len + l > available {
238+ break ;
239+ }
240+ prefix_len += l;
241+ }
242+
243+ let ( prefix, remainder) = string. split_at ( prefix_len) ;
244+ // Prefix is constructed from `chars()` boundaries, so it is valid UTF-8 and fits by construction.
245+ self . try_insert_str ( idx, prefix) ?;
246+ Ok ( remainder)
247+ }
248+
249+ /// Inserts a string slice at the given byte index, truncating to capacity, panicking on invalid index/boundary.
250+ ///
251+ /// Returns the remainder that did not fit.
252+ #[ inline]
253+ pub fn insert_str_truncated < ' s > ( & mut self , idx : usize , string : & ' s str ) -> & ' s str {
254+ self . try_insert_str_truncated ( idx, string)
255+ . expect ( "invalid index or char boundary" )
256+ }
257+
258+ /// Inserts a string slice at the given byte index, panicking if the capacity would be exceeded.
259+ ///
260+ /// This is an explicit opt-in panicking API for fixed-capacity strings.
261+ #[ inline]
262+ pub fn insert_str_expect_capacity ( & mut self , idx : usize , string : & str ) {
263+ self . try_insert_str ( idx, string)
264+ . expect ( "PascalString insert failed" ) ;
265+ }
266+
267+ /// Inserts a string slice at the given byte index, panicking if the capacity would be exceeded.
268+ #[ inline]
269+ #[ deprecated( note = "PascalString is fixed-capacity; prefer `try_insert_str`, `try_insert_str_truncated`, or `insert_str_expect_capacity`." ) ]
270+ pub fn insert_str ( & mut self , idx : usize , string : & str ) {
271+ self . insert_str_expect_capacity ( idx, string) ;
272+ }
273+
274+ /// Inserts a character at the given byte index.
275+ ///
276+ /// This is a true `try_` API: it **never panics**. All failure modes are returned as `InsertError`.
277+ #[ inline]
278+ pub fn try_insert ( & mut self , idx : usize , ch : char ) -> Result < ( ) , InsertError > {
279+ let mut buf = [ 0_u8 ; 4 ] ;
280+ let s = ch. encode_utf8 ( & mut buf) ;
281+ self . try_insert_str ( idx, s)
282+ }
283+
284+ /// Inserts a character at the given byte index, panicking if the capacity would be exceeded.
285+ #[ inline]
286+ pub fn insert_expect_capacity ( & mut self , idx : usize , ch : char ) {
287+ self . try_insert ( idx, ch)
288+ . expect ( "PascalString insert failed" ) ;
289+ }
290+
291+ /// Inserts a character at the given byte index, panicking if the capacity would be exceeded.
292+ #[ inline]
293+ #[ deprecated( note = "PascalString is fixed-capacity; prefer `try_insert`, `try_insert_str_truncated`, or `insert_expect_capacity`." ) ]
294+ pub fn insert ( & mut self , idx : usize , ch : char ) {
295+ self . insert_expect_capacity ( idx, ch) ;
296+ }
297+
298+ /// Removes and returns the `char` at the given byte index.
299+ ///
300+ /// # Panics
301+ ///
302+ /// - If `idx >= self.len()`
303+ /// - If `idx` is not on a UTF-8 character boundary
304+ #[ inline]
305+ pub fn remove ( & mut self , idx : usize ) -> char {
306+ let len = self . len ( ) ;
307+ assert ! ( idx < len, "index out of bounds" ) ;
308+ assert ! ( self . is_char_boundary( idx) , "index is not a char boundary" ) ;
309+
310+ let ch = self . as_str ( ) [ idx..] . chars ( ) . next ( ) . expect ( "idx < len" ) ;
311+ let ch_len = ch. len_utf8 ( ) ;
312+
313+ // Shift tail left to close the gap.
314+ self . data . copy_within ( idx + ch_len..len, idx) ;
315+ let new_len = len - ch_len;
316+ self . len = new_len as u8 ;
317+
318+ // Keep deterministic contents beyond len (not required for soundness, but helps debugging/tests).
319+ self . data [ new_len..len] . fill ( 0 ) ;
320+
321+ ch
322+ }
323+
324+ /// Removes and returns the `char` at the given byte index.
325+ ///
326+ /// This is a true `try_` API: it **never panics**. All failure modes are returned as `RemoveError`.
327+ #[ inline]
328+ pub fn try_remove ( & mut self , idx : usize ) -> Result < char , RemoveError > {
329+ let len = self . len ( ) ;
330+ if idx >= len {
331+ return Err ( RemoveError :: OutOfBounds { idx, len } ) ;
332+ }
333+ if !self . is_char_boundary ( idx) {
334+ return Err ( RemoveError :: NotCharBoundary { idx } ) ;
335+ }
336+ Ok ( self . remove ( idx) )
337+ }
338+
166339 /// Returns the remainder of the string that was not pushed.
167340 #[ inline]
168341 pub fn push_str_truncated < ' s > ( & mut self , string : & ' s str ) -> & ' s str {
@@ -632,11 +805,30 @@ mod tests {
632805 fn test_push_str_panics_on_overflow ( ) {
633806 let result = std:: panic:: catch_unwind ( || {
634807 let mut ps = PascalString :: < 4 > :: new ( ) ;
635- ps. push_str ( "abcde" ) ;
808+ ps. push_str_expect_capacity ( "abcde" ) ;
636809 } ) ;
637810 assert ! ( result. is_err( ) ) ;
638811 }
639812
813+ #[ test]
814+ fn test_insert_str_and_remove_unicode_boundaries ( ) {
815+ let mut ps = PascalString :: < 8 > :: try_from ( "ab" ) . unwrap ( ) ;
816+ ps. insert_str_expect_capacity ( 1 , "€" ) ; // 3 bytes
817+ assert_eq ! ( ps. as_str( ) , "a€b" ) ;
818+
819+ let removed = ps. remove ( 1 ) ;
820+ assert_eq ! ( removed, '€' ) ;
821+ assert_eq ! ( ps. as_str( ) , "ab" ) ;
822+ }
823+
824+ #[ test]
825+ fn test_try_insert_str_too_long_does_not_modify ( ) {
826+ let mut ps = PascalString :: < 4 > :: try_from ( "ab" ) . unwrap ( ) ;
827+ let err = ps. try_insert_str ( 1 , "€" ) . unwrap_err ( ) ; // would become 5 bytes
828+ assert_eq ! ( err, InsertError :: TooLong ) ;
829+ assert_eq ! ( ps. as_str( ) , "ab" ) ;
830+ }
831+
640832 #[ test]
641833 fn test_try_from_str_const ( ) {
642834 const PS : Option < PascalString < 4 > > = PascalString :: < 4 > :: try_from_str_const ( "ab" ) ;
0 commit comments