Rust's String/str maintains valid UTF-8; operating system paths, however, are not necessarily representable as UTF-8. You must first determine whether the data represents text, raw bytes, or a system path to choose types and operations that do not lose information.
Ownership and Borrowing are the First Axis
Owned Content
Borrowed View
Guarantee
String
str / &str
Valid UTF-8 text
Vec
[u8] / &[u8]
Arbitrary bytes, no promise of text encoding
OsString
OsStr / &OsStr
Platform string, preserving what it can express
PathBuf
Path / &Path
Platform path component semantics
&str can borrow a portion of a String; it does not only point to string literals, nor does it automatically have a 'static lifetime. String's safe APIs maintain the UTF-8 invariant; when constructed or modified using unsafe, the caller must continue to maintain it. The compiler does not automatically verify all memory operations. String
Bytes, char, and Visible Characters are Different
fnmain(){let text ="e\u{301}中";assert_eq!(text.len(),6);assert_eq!(text.chars().count(),3);assert_eq!(text.char_indices().collect::<Vec<_>>(),[(0,'e'),(1,'\u{301}'),(3,'中')]);assert_eq!(text.get(1..3),Some("\u{301}"));assert_eq!(text.get(2..3),None);// 2 is in the middle of the UTF-8 encoding
}
len counts bytes, while chars iterates over Unicode scalar values. The term "code point" in Unicode also includes values like surrogates that cannot be represented as Rust char; the concept of "scalar value" should be used for precise discussion.
A character perceived by the user may consist of multiple scalar values, and emoji sequences do not necessarily consist of a single 4-byte scalar. Tasks such as editor cursor movement or truncating user nicknames usually require grapheme cluster segmentation rules; display column width is yet another issue. Unicode Text Segmentation
Indexing Requires Selecting a Unit First
Rust does not provide a text[n] interface that returns the "n-th character," because this term might refer to bytes, scalar values, or grapheme clusters, each with different costs and semantics.
Read the n-th byte: text.as_bytes().get(n).
Read the n-th scalar value: text.chars().nth(n), which usually requires scanning preceding encodings.
Borrow text by byte range: text.get(a..b), requiring the range and UTF-8 boundaries to be valid.
For grapheme clusters: Use a library implementing the corresponding Unicode segmentation rules, and be aware of its Unicode version.
Direct indexing &text[a..b] will panic if the range or boundary is invalid; get uses Option to express failure. Repeatedly calling chars().nth(i) in a loop may result in quadratic complexity due to repeated scanning; it is usually better to complete the work in a single chars or char_indices iteration.
Valid UTF-8 does not mean the text is already Unicode normalized: pre-composed é and e plus combining accent are typically not considered equal in standard string equality comparisons. The normalization strategy required for searching or identity matching should be explicitly decided by the business logic.
Finding Valid Boundaries Forward When Byte Budget is Limited
fnprefix_bytes(text:&str, budget:usize)->&str{letmut end = budget.min(text.len());while!text.is_char_boundary(end){
end -=1;}&text[..end]}fnmain(){assert_eq!(prefix_bytes("中ab",2),"");assert_eq!(prefix_bytes("中ab",4),"中a");assert_eq!(prefix_bytes("中ab",100),"中ab");assert_eq!(prefix_bytes("e\u{301}",1),"e");}
This satisfies the contract of "not exceeding the specified UTF-8 byte count and resulting in valid text." The last assertion also exposes its boundary: it can cut off the combining accent from the letter. This might be suitable for byte limits on network fields, but for user-visible nicknames, truncation points should be decided according to grapheme cluster rules. The return value is a borrow of the original text, with no new String created; consequently, the source string cannot be arbitrarily modified while the borrow is still in use.
Text Equality Does Not Automatically Perform Normalization
Choosing between lines and split also depends on the protocol: the former handles line semantics, while the latter preserves trailing empty segments based on the delimiter character. When empty fields need to be preserved or exact raw newlines are required, do not arbitrarily replace one with the other for the sake of brevity. Normalization, case handling, and whitespace stripping are also data interpretation strategies that should be defined at the input boundary, preserving the original representation needed for display or auditing.
UTF-8 boundaries differ from visible-character boundaries
Byte-budget truncation asks two questions: preserve valid UTF-8, then decide whether splitting a user-visible character is acceptable. Budgets 2 or 3 cut inside 中; budget 5 is valid UTF-8 but removes e’s accent. These boundaries need different policies.
Preparing the visual
UTF-8 boundaries differ from visible-character boundaries
Valid UTF-8 truncation preserves scalar encoding but can separate e from its combining accent.
{"id":"rust-utf8-boundaries","title":"UTF-8 boundaries differ from visible-character boundaries","summary":"Valid UTF-8 truncation preserves scalar encoding but can separate e from its combining accent.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"budget\" data-i18n=\"budget\"></label><output for=\"budget\" id=\"budget-value\"></output><input id=\"budget\" type=\"range\" min=\"0\" max=\"7\" value=\"5\" step=\"1\"></div><div class=\"track-row\"><span data-i18n=\"bytes\"></span><div id=\"bytes\" class=\"track\"></div></div><section class=\"panel\"><h3 data-i18n=\"scalar\"></h3><div id=\"scalar\"></div></section><section class=\"panel\"><h3 data-i18n=\"grapheme\"></h3><div id=\"grapheme\"></div></section><div class=\"metrics\"><div><span data-i18n=\"direct\"></span><strong id=\"direct\"></strong></div><div><span data-i18n=\"safe\"></span><strong id=\"safe\"></strong></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"ok\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"split\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"invalid\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:550;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:550 20px ui-monospace,monospace}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:ui-monospace,monospace;font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px ui-monospace,monospace;background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px ui-monospace,monospace}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 ui-monospace,monospace;white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:ui-monospace,monospace}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px ui-monospace,monospace;padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 ui-monospace,monospace;overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n.chart-wrap{margin:18px 0}.chart-label{font-size:12px;color:var(--muted);min-height:3.2em}.curve{display:block;width:100%;height:auto;max-height:320px;overflow:visible}.curve text{font:11px ui-monospace,monospace;fill:var(--muted)}.gridline{stroke:var(--rule);stroke-width:1}.metrics strong{font-size:20px;min-height:3.2em}.formula{font:12px/1.8 ui-monospace,monospace;min-height:9em}.readout{min-height:8em}.presets button{flex-basis:30%}.scope p{overflow-wrap:anywhere} .unit-note{font-size:12px;color:var(--muted);margin:10px 0}\n\n.metrics span{word-break:break-all;line-break:strict}.metrics>div{padding-right:6px}\n\n.cell{min-height:72px}.metrics strong{min-height:3.6em}\n\n.track{flex-wrap:wrap}.cell{flex-basis:20%}.metrics strong{font-size:16px;min-height:4em}.panel>div{font:13px ui-monospace,monospace;min-height:2.4em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';row.style.gridTemplateColumns='repeat('+r.length+',minmax(0,1fr))';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}function plot(id,series,xr,yr,opts={}){const e=$('#'+id),X=x=>42+(x-xr[0])/(xr[1]-xr[0])*298,Y=y=>175-(y-yr[0])/(yr[1]-yr[0])*148;let out='<defs><clipPath id=\"clip-'+id+'\"><rect x=\"42\" y=\"27\" width=\"298\" height=\"148\"/></clipPath></defs>';for(let i=0;i<3;i++){let x=xr[0]+i*(xr[1]-xr[0])/2,y=yr[0]+i*(yr[1]-yr[0])/2;out+='<path class=\"gridline\" d=\"M '+X(x)+' 27V175M42 '+Y(y)+'H340\"/><text x=\"'+X(x)+'\" y=\"194\" text-anchor=\"middle\">'+esc(opts.xfmt?opts.xfmt(x):Number(x.toFixed(2)))+'</text><text x=\"36\" y=\"'+(Y(y)+4)+'\" text-anchor=\"end\">'+esc(opts.yfmt?opts.yfmt(y):Number(y.toFixed(2)))+'</text>';}out+='<g clip-path=\"url(#clip-'+id+')\">';for(const s of series){out+='<path fill=\"none\" stroke=\"'+(s.color||'var(--accent)')+'\" stroke-width=\"2.5\" '+(s.dash?'stroke-dasharray=\"5 4\"':'')+' d=\"'+s.pts.map((p,i)=>(i?'L':'M')+X(p[0]).toFixed(2)+','+Y(p[1]).toFixed(2)).join(' ')+'\"/>';}for(const p of opts.points||[])out+='<circle cx=\"'+X(p[0])+'\" cy=\"'+Y(p[1])+'\" r=\"4\" fill=\"var(--second)\" stroke=\"var(--paper)\" stroke-width=\"1.5\"/>';if(opts.cursor!==undefined)out+='<path d=\"M'+X(opts.cursor)+' 27V175\" stroke=\"var(--muted)\" stroke-dasharray=\"3 3\"/>';e.innerHTML=out+'</g>';e.dataset.curves=JSON.stringify(series.map(s=>s.pts));}const curve=(fn,lo,hi,n=120)=>Array.from({length:n+1},(_,i)=>{const x=lo+(hi-lo)*i/n;return[x,fn(x)]});function inputs(draw){document.querySelectorAll('input,select').forEach(e=>{e.addEventListener('input',draw);e.addEventListener('change',draw)});draw()}const n=id=>+$('#'+id).value;const show=(id,v,d=3)=>set(id,Number(v.toFixed(d)));function draw(){let k=n('budget'),bs=[0x41,0xe4,0xb8,0xad,0x65,0xcc,0x81],bounds=[0,1,4,5,7],end=Math.max(...bounds.filter(x=>x<=k)),decoder=new TextDecoder(),s=decoder.decode(new Uint8Array(bs.slice(0,end))),valid=bounds.includes(k);set('budget-value',k+' B');cells('bytes',bs.map((b,i)=>({text:i+'\\n'+b.toString(16).toUpperCase(),cls:i<end?'on':i<k?'gold':'empty'})));set('scalar','0 │ A │ 1 │ 中 │ 4 │ e │ 5 │ ◌́ │ 7');set('grapheme','0 │ A │ 1 │ 中 │ 4 │ é │ 7');set('direct',valid?'Some(\"'+s+'\")':'None');set('safe','\"'+s+'\" ('+end+' B)');set('explanation',t(!valid?'invalid':end===5?'split':'ok'));document.body.dataset.end=end;}inputs(draw);","audio":false,"strings":{"budget":"Byte budget","bytes":"UTF-8 bytes and offsets","direct":"text.get(..budget)","grapheme":"Grapheme boundaries","heading":"UTF-8 boundaries differ from visible-character boundaries","intro":"Move a byte budget; compare get(..budget) with rounding down to a valid UTF-8 boundary.","invalid":"The budget lands inside a multibyte encoding, so get returns None; rounding down yields valid text.","ok":"This is both a UTF-8 and a grapheme boundary in the example.","reset":"Reset","safe":"Text after boundary rounding","scalar":"Scalar boundaries","scope":"Fixed text A中é with U+0301; grapheme boundaries are annotated for this example, not a general Unicode segmenter.","scopeLabel":"Model scope and assumptions","split":"The result is valid UTF-8, but e loses its combining accent, splitting a visible character."}}
Converting Bytes to Text: Clarify Whether Loss is Allowed
String::from_utf8 checks the owned byte vector and returns the original bytes in the error on failure; str::from_utf8 checks borrowed slices. from_utf8_lossy displays invalid sequences using replacement characters, which is suitable for some logs but not for protocols or filenames requiring lossless round-tripping.
String does not guarantee a trailing C-style NUL byte. Use CString/CStr for interoperability with C strings, and consider internal NULs and external buffer lifetimes. See C ABI and bindgen.
Distinguish Between Missing Subsequent Bytes and Definitively Invalid Encodings
fnmain(){letmut bytes =vec![b'A',0xe4,0xb8];let error =std::str::from_utf8(&bytes).unwrap_err();assert_eq!(error.valid_up_to(),1);assert_eq!(error.error_len(),None);
bytes.extend([0xad,b'B']);assert_eq!(std::str::from_utf8(&bytes),Ok("A中B"));let bad =vec![b'A',0xff,b'B'];let error =String::from_utf8(bad).unwrap_err();assert_eq!(error.utf8_error().valid_up_to(),1);assert_eq!(error.utf8_error().error_len(),Some(1));assert_eq!(error.into_bytes(),[b'A',0xff,b'B']);}
When reading the first chunk, the trailing E4 B8 might just be the character '中' spanning chunks; it cannot be immediately discarded or replaced. If the protocol specifies a UTF-8 stream, you can output the confirmed prefix, retain the incomplete tail, and continue checking with the next chunk; if the message or stream has ended, you must report a truncation or execute a specific replacement strategy. Applying from_utf8_lossy to each read chunk directly would turn originally valid cross-chunk characters into replacement characters. Utf8Error
This example stores all bytes in a Vec to demonstrate boundaries. A real streaming implementation must also limit the cumulative buffer size and avoid re-validating the long confirmed prefix from scratch on every append.
OsStr Does Not Promise a Specific Persistable Internal Encoding
Unix filename components can contain non-UTF-8 bytes but cannot contain NUL, and slashes are path separators. Windows' system wide-character interface uses 16-bit encoding units, which may contain content that cannot be treated as valid Unicode scalar sequences; the Windows filesystem native format cannot simply be called WTF-8.
Rust's internal Windows representation has an implementation relationship with WTF-8, but the cross-platform OsStr abstraction does not allow applications to rely on a specific internal encoding as a persistence protocol. On Unix, you can use OsStrExt::as_bytes or similar platform interfaces for lossless access; on Windows, you can use encode_wide to get wide character units. OsStr, Windows OsStrExt
Path::display and to_string_lossy are suitable for human reading and should not be treated as the original path identity. For file operations, try to pass &Path/PathBuf throughout to avoid UTF-8 conversion round-trips.
Path Operations Do Not Automatically Access or Isolate the Filesystem
join, parent, extension, components, etc., primarily handle platform path syntax. If the right-hand side of join is an absolute path, it replaces the preceding base path; Windows prefixes and root paths also have platform-specific rules. Path
components does not arbitrarily fold a/../b because symbolic links might change the meaning. canonicalize accesses the filesystem and resolves the path, but the result does not prove that the directory and links haven't changed during subsequent access.
Therefore, PathBuf::join is the correct tool for expressing path concatenation, but it is not a "safe boundary that guarantees user input always stays within a certain directory." If the task requires limiting access to a root directory, you must use appropriate directory handles and resolution constraints according to the target system; a simple string prefix check is insufficient.
Path Prefixes are Compared by Component, Still Not Representing Filesystem Containment
The function only expresses a lexical input strategy: non-empty, and all parsed components are Normal. components performs limited syntactic normalization; original spelling information like repeated separators may not be preserved; different platforms use their own separators and prefix rules. It does not check if the path exists, nor can it prevent symbolic links within reports from pointing outside.
Path::starts_with is more aligned with component semantics than string prefixes, so reports-old is not considered a sub-path of reports; but paths with .. can still pass this component prefix check. Even if .. is rejected, symbolic links and concurrent directory changes still need to be handled during the actual opening phase; normal_relative cannot be treated as a complete file access isolation solution. Path Components and Prefixes
Separate Display, Storage, and Operation Interfaces
Use Case
Suitable Representation
What to Preserve During Conversion
Text protocol fields
&str / String
Encoding errors, length units, and normalization strategies
Unknown encoding input
&[u8] / Vec
Raw bytes, decode only after confirming format
Current platform file operations
&Path / PathBuf
Original OsStr content and platform component semantics
Filenames in logs
display or explicit escaped representation
Display may be lossy; no longer used to locate the original file
Cross-platform path identifiers
Explicitly designed format
Platform, root/prefix rules, and handling of unrepresentable content
When file APIs receive paths, prioritize having callers pass AsRef<Path> or &Path, and perform path operations internally; if you force receiving a String, you exclude some valid system paths at the entry point. Conversely, resource names in network protocols that look like paths should first be interpreted according to the protocol specifications; do not directly apply the current OS's Path rules.